Статьи

Модульное тестирование JPA … Прекратите интеграционное тестирование!

Я хочу начать с простого вопроса.

«Как вы тестируете свои классы JPA?»

Теперь, прежде чем ответить, внимательно посмотрите на вопрос. Ключевые слова — юнит тест . Не тест, а юнит тест .

По моему опыту, после того, как задать этот вопрос, разговор идет примерно так.

«Как вы тестируете свои доменные объекты JPA?»

«Мы разработали этот общий проект, который запускает базу данных Derby в памяти и автоматически запускает сценарии в вашем проекте для создания базы данных и вставки данных для тестов».

«Где этот общий проект?»

«Это в Nexus. Мы включаем его как зависимость Maven».

«Я вижу это в POM. Вы знаете, что эта зависимость не имеет <scope> test </ scope>»

«А?»

«Не бери в голову. Где исходный код проекта?»

«Человек, который сделал это, здесь больше не работает, поэтому мы не знаем, где находится исходный код. Но нам не пришлось его менять».

«Где документация?»

«Хмм … мы просто копируем материал из существующих проектов и меняем DDL и запросы»

«Почему вы запускаете Derby для модульного теста? Модульные тесты должны быть простыми в обслуживании и быстрыми для запуска. Вы не можете запускать какие-либо фреймворки, такие как JPA, или полагаться на внешние ресурсы, такие как база данных. Они делают модульные тесты сложными и медленными. Бег.»

«Ну, классы используют JPA, вам нужна база данных для их проверки».

«Нет, не нужно. Вам не нужно запускать базу данных. JPA в значительной степени опирается на аннотации. Все, что вам нужно сделать, это убедиться, что все классы, поля и получатели правильно аннотированы. Так что просто тестируйте аннотации и значения их свойств «.

«Но это не скажет вам, работает ли он с базой данных».

«Итак? Вы должны писать простые и быстрые модульные тесты! Не интеграционные тесты! Для модульного теста все, что вам нужно знать, это то, правильно ли аннотированы классы JPA. Если они аннотированы правильно, они будут работать».

«Но что, если базы данных изменятся?»

«Хороший вопрос, но не для модульного теста. Для модульного теста все, что вам нужно знать, это то, что то, что работало раньше, все еще работает должным образом. Для сред, таких как JPA, которые зависят от аннотаций, чтобы работать должным образом, ваши модульные тесты должны убедиться, что аннотации не были перепутаны «.

«Но как вы узнаете, правильны ли аннотации? Вы должны запустить базу данных, чтобы получить их правильно».

«Ну, а если бы вы не использовали JPA, а что, если бы вы писали SQL вручную? Вы бы написали модульный тест для подключения к базе данных и продолжали возиться с SQL в своем коде, пока не поняли его правильно? Конечно нет. Это было бы безумием! Вместо этого вы должны использовать такой инструмент, как SQL Developer, подключиться к базе данных и работать над запросом до тех пор, пока он не будет работать правильно. Затем, после того, как вы получите правильный запрос, вы скопируйте и вставьте запрос в свой код. Вы знаете, что запрос работает — вы только что запустили его в SQL Developer — поэтому вам вообще не нужно подключаться к базе данных из вашего модульного теста. Ваш модульный тест должен только утверждать, что код генерирует запрос правильно. Если вы используете JPA, это в основном то же самое. Разница в том, что с JPA вы должны получить правильные аннотации. Итак, JPA работает где-то еще,затем, когда вы получите это правильно, скопируйте и вставьте его в свой проект и протестируйте аннотации ».

«Но где вы делаете эту работу? Может ли SQL Developer помочь выяснить аннотации JPA? …. Подождите! Я думаю, что Жаба может. У нас есть больше лицензий для этого?»

«Тьфу! Нет! Вы создаете исследовательский проект JPA, который запускает реализацию JPA, чтобы вы могли поиграться с аннотациями JPA. В этом исследовательском проекте в идеале вы должны подключиться к реальной базе данных разработки проекта, но вы можете подключиться к любому База данных, которая содержит данные, которые вам нужны. Выполнение всей этой работы в исследовательском проекте на самом деле намного лучше для реального проекта, потому что вы избавляетесь от базы данных в памяти от реального проекта, а также избавляетесь от попыток воспроизвести реальный проект. база данных в дерби.

«Где мы можем получить такой исследовательский проект?»

«Хм, вы просто создаете его; щелкните правой кнопкой мыши -> Создать -> Новый проект».

«Вы имеете в виду, что каждый должен создать свой собственный исследовательский проект? Похоже на пустую трата времени».

«Тьфу!»

Если у вас был подобный разговор, пожалуйста, дайте мне знать. Я хотел бы услышать ваши истории.

Но с учетом всего сказанного, как вы тестируете аннотации ваших объектов JPA. Ну, это не так уж сложно. API отражения Java предоставляют доступ к аннотациям классов. Итак, давайте посмотрим, как это может выглядеть.

Предположим, листинг 1 является объектом Person. Этот объект Person является частью вашей доменной модели и настроен для обработки JPA для сохранения данных в базе данных.

Листинг 1. Объектная модель Person и Phone

package org.thoth.jpa.UnitTesting;

import java.util.ArrayList;
import java.util.List;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.OneToMany;
import javax.persistence.Table;

/**
 * @author Michael Remijan mjremijan@yahoo.com @mjremijan
 */
@Entity
@Table(name = "T_PERSON")
  public class Person {

  private Long id;
  private String firstName;
  private String lastName;
  private List<Phone> phones = new ArrayList<>();

  @Id
  @GeneratedValue()
  public Long getId() {
    return id;
  }

  public void setId(Long id) {
    this.id = id;
  }

  @Column(name = "FIRST_NAME")
  public String getFirstName() {
    return firstName;
  }

  public void setFirstName(String firstName) {
    this.firstName = firstName;
  }

  @Column(name = "LAST_NAME")
  public String getLastName() {
    return lastName;
  }

  public void setLastName(String lastName) {
    this.lastName = lastName;
  }

  @OneToMany(mappedBy = "person", fetch = FetchType.LAZY)
  public List<Phone> getPhones() {
    return phones;
  }
}

Код в листинге 1 является лишь примером, поэтому он очень прост. В реальных приложениях доменные объекты и их связи с другими объектами станут сложными. Но этого достаточно для демонстрационных целей. Теперь, следующее, что вы хотите сделать, это модульное тестирование этого объекта. Помните, что ключевые слова — юнит-тест. Вы не хотите запускать какие-либо фреймворки или базы данных. Это аннотации и их свойства, которые заставляют объект Person работать должным образом, так что это то, что вы хотите проверить модулем. В листинге 2 показано, как может выглядеть модульный тест для объекта Person.

Листинг 2: UnitTest Unit Test

package org.thoth.jpa.UnitTesting;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.OneToMany;
import javax.persistence.Table;
import org.junit.Assert;
import org.junit.Test;

/**
 * @author Michael Remijan mjremijan@yahoo.com @mjremijan
 */
public class PersonTest {
  @Test
  public void typeAnnotations() {
    // assert
    AssertAnnotations.assertType(
        Person.class, Entity.class, Table.class);
  }


  @Test
  public void fieldAnnotations() {
    // assert
    AssertAnnotations.assertField(Person.class, "id");
    AssertAnnotations.assertField(Person.class, "firstName");
    AssertAnnotations.assertField(Person.class, "lastName");
    AssertAnnotations.assertField(Person.class, "phones");
  }


  @Test
  public void methodAnnotations() {
    // assert
    AssertAnnotations.assertMethod(
        Person.class, "getId", Id.class, GeneratedValue.class);

    AssertAnnotations.assertMethod(
        Person.class, "getFirstName", Column.class);

    AssertAnnotations.assertMethod(
        Person.class, "getLastName", Column.class);

    AssertAnnotations.assertMethod(
        Person.class, "getPhones", OneToMany.class);
  }


  @Test
  public void entity() {
    // setup
    Entity a
    = ReflectTool.getClassAnnotation(Person.class, Entity.class);

    // assert
    Assert.assertEquals("", a.name());
  }


  @Test
  public void table() {
    // setup
    Table t
    = ReflectTool.getClassAnnotation(Person.class, Table.class);

    // assert
    Assert.assertEquals("T_PERSON", t.name());
  }


  @Test
  public void id() {
    // setup
    GeneratedValue a
    = ReflectTool.getMethodAnnotation(
        Person.class, "getId", GeneratedValue.class);

    // assert
    Assert.assertEquals("", a.generator());
    Assert.assertEquals(GenerationType.AUTO, a.strategy());
  }


  @Test
  public void firstName() {
    // setup
    Column c
    = ReflectTool.getMethodAnnotation(
        Person.class, "getFirstName", Column.class);

    // assert
    Assert.assertEquals("FIRST_NAME", c.name());
  }


  @Test
  public void lastName() {
    // setup
    Column c
    = ReflectTool.getMethodAnnotation(
        Person.class, "getLastName", Column.class);

    // assert
    Assert.assertEquals("LAST_NAME", c.name());
  }


  @Test
  public void phones() {
    // setup
    OneToMany a
    = ReflectTool.getMethodAnnotation(
        Person.class, "getPhones", OneToMany.class);

    // assert
    Assert.assertEquals("person", a.mappedBy());
    Assert.assertEquals(FetchType.LAZY, a.fetch());
  }
}

Для этого модульного теста я создал пару простых вспомогательных классов: AssertAnnotations и ReflectTool, поскольку они, очевидно, могут быть повторно использованы в других тестах. AssertAnnotations и ReflectTool показаны в листинге 3 и 4 соответственно. Но прежде чем перейти к этим вспомогательным классам, давайте посмотрим на PersonTest более подробно.

Строка 19 — это метод #typeAnnotations. Этот метод устанавливает аннотации для самого класса Person. Строка 21 вызывает метод #assertType и передает Person.class в качестве первого параметра, а затем список аннотаций, ожидаемых для класса. Важно отметить, что метод #assertType проверит, что переданные ему аннотации являются единственными аннотациями в классе. В этом случае Person.class должен иметь только аннотации Entity и Table. Если кто-то добавляет аннотацию или удаляет аннотацию, #assertType выдаст ошибку AssertionError.

Строка 27 — это метод #fieldAnnotations. Этот метод устанавливает аннотации к полям класса Person. Строки 29-32 вызывают метод #assertField. Первый параметр это Person.class. Второй параметр — это имя поля. Но потом чего-то не хватает; где находится список аннотаций? Ну, в этом случае нет аннотаций! Ни одно из полей в этом классе не аннотировано. Не передавая аннотации методу #assertField, он проверит, чтобы убедиться, что в поле нет аннотаций. Конечно, если ваш JPA-объект использует аннотации на полях вместо метода-получателя, вы должны добавить в список ожидаемых аннотаций. Важно отметить, что метод #assertField будет проверять, что переданные ему аннотации являются единственными аннотациями в поле. Если кто-то добавляет аннотацию или удаляет аннотацию,#assertField выдаст ошибку AssertionError.

Строка 37 — это метод #methodAnnotations. Этот метод устанавливает аннотации для методов-получателей класса Person. Строки 39-49 вызывают метод #assertMethod. Первый параметр это Person.class. Второй параметр — это имя метода получения. Остальные параметры являются ожидаемыми аннотациями. Важно отметить, что метод #assertMethod проверит, что переданные ему аннотации являются единственными аннотациями в геттере. Если кто-то добавляет аннотацию или удаляет аннотацию, #assertMethod выдаст ошибку AssertionError. Например, в строке 40 метод getId должен иметь только аннотации Id и GeneratedValue, а другие нет.

На этом этапе PersonTest установил аннотации для класса, его полей и методов получения. Но аннотации тоже имеют значения. Например, строка 17 класса Person имеет вид @Table (name = «T_PERSON»). Имя таблицы жизненно важно для правильной работы этого объекта JPA, поэтому при тестировании должен быть проверен модульный тест.

Строка 64 — это метод #table. Он использует ReflectTool в строке 68, чтобы получить аннотацию Table от класса Person. Затем в строке 71 утверждается, что именем таблицы является «T_PERSON».

Остальная часть метода модульного теста в PersonTest утверждает значения аннотаций в классе Person. В строке 83 утверждается, что аннотация GeneratedValue не имеет генератора, а в строке 84 указывается тип генерации. Строки 96 и 108 утверждают имена столбцов таблицы базы данных. Строки 120-121 утверждают тип отношений между объектом Person и объектом Phone.

Посмотрев на PersonTest более подробно, давайте посмотрим на справочные классы: AssertAnnotations и ReflectTool. Я не собираюсь ничего говорить об этих классах; они не все такие сложные.

Листинг 3: Помощник AssertAnnotations

package org.thoth.jpa.UnitTesting;

import java.lang.annotation.Annotation;
import java.util.Arrays;
import java.util.List;

/**
 * @author Michael Remijan mjremijan@yahoo.com @mjremijan
 */
public class AssertAnnotations {
  private static void assertAnnotations(
      List<Class> annotationClasses, List<Annotation> annotations) {
    // length
    if (annotationClasses.size() != annotations.size()) {
      throw new AssertionError(
        String.format("Expected %d annotations, but found %d"
          , annotationClasses.size(), annotations.size()
      ));
    }

    // exists
    annotationClasses.forEach(
      ac -> {
        long cnt
          = annotations.stream()
            .filter(a -> a.annotationType().isAssignableFrom(ac))
            .count();
        if (cnt == 0) {
          throw new AssertionError(
            String.format("No annotation of type %s found", ac.getName())
          );
        }
      }
    );
  }


  public static void assertType(Class c, Class... annotationClasses) {
    assertAnnotations(
        Arrays.asList(annotationClasses)
      , Arrays.asList(c.getAnnotations())
    );
  }


  public static void assertField(
      Class c, String fieldName, Class... annotationClasses) {
    try {
      assertAnnotations(
        Arrays.asList(annotationClasses)
        , Arrays.asList(c.getDeclaredField(fieldName).getAnnotations())
      );
    } catch (NoSuchFieldException nsfe) {
      throw new AssertionError(nsfe);
    }
  }


  public static void assertMethod(
      Class c, String getterName, Class...annotationClasses) {
    try {
      assertAnnotations(
        Arrays.asList(annotationClasses)
        , Arrays.asList(c.getDeclaredMethod(getterName).getAnnotations())
      );
    } catch (NoSuchMethodException nsfe) {
      throw new AssertionError(nsfe);
    }
  }
}

Листинг 4: Помощник ReflectTool

package org.thoth.jpa.UnitTesting;

import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.Method;

/**
 * @author Michael Remijan mjremijan@yahoo.com @mjremijan
 */
public class ReflectTool {
  public static <T extends Annotation> T getMethodAnnotation(
      Class<?> c, String methodName, Class<T> annotation) {
    try {
      Method m = c.getDeclaredMethod(methodName);
      return (T)m.getAnnotation(annotation);
    } catch (NoSuchMethodException nsme) {
      throw new RuntimeException(nsme);
    }
  }

  public static <T extends Annotation> T getFieldAnnotation(
      Class<?> c, String fieldName, Class<T> annotation) {
    try {
      Field f = c.getDeclaredField(fieldName);
      return (T)f.getAnnotation(annotation);
    } catch (NoSuchFieldException nsme) {
      throw new RuntimeException(nsme);
    }
  }

  public static <T extends Annotation> T getClassAnnotation(
      Class<?> c, Class<T> annotation) {
    return (T) c.getAnnotation(annotation);
  }
}

Вот и все. Я надеюсь, что это полезно.

Ссылки
https://www.javacodegeeks.com/2015/02/jpa-tutorial.html#relationships_onetomany