Рубрики
Без рубрики

Тестирование весенней загрузки — Данные и сервисы

Тестирование весенней загрузки — Данные и сервисы Я думаю, что тестирование – это важная вещь в программном обеспечении… Помечено как тестирование, java, учебник, база данных.

Тестирование весенней загрузки — Данные и сервисы

Я думаю, что тестирование – это важная вещь в разработке программного обеспечения. И я не единственный такой. Если вы спросите любого разработчика, важны ли тесты или нет, они, вероятно, скажут вам то же самое.

Но реальность не так радужна. Почти во всех проектах, которые я видел, отсутствует либо наличие тестов, либо их качество. Это не просто один случай. Проблема носит системный характер.

Почему это происходит? Я считаю, что разработчики обычно не уделяют достаточного внимания улучшению знаний об использовании фреймворков тестирования. Поэтому, когда дело доходит до проверки бизнес-логики, программисты просто не знают, как это сделать.

Давайте восполним пробелы и посмотрим, что приготовил для нас Весенний тест.

Фрагменты кода взяты из этого репозитория . Вы можете клонировать его и запускать тесты, чтобы увидеть, как он работает.

Сервисный уровень + Издевается

Издевательства стали настолько широко распространены в средах тестирования, что издевательство и тестирование считаются почти синонимами.

Предположим, что у нас есть Человек, создающий сервис .

@Service
@RequiredArgsConstructor
public class PersonCreateServiceImpl implementes PersonCreateService {

  private final PersonValidateService personValidateService;
  private final PersonRepository personRepository;

  @Override
  @Transactional
  public PersonDTO createPerson(String firstName, String lastName) {
    personValidateService.checkUserCreation(firstName, lastName);
    final var createdPerson = personRepository.saveAndFlush(
        new Person()
            .setFirstName(firstName)
            .setLastName(lastName)
    );
    return DTOConverters.toPersonDTO(createdPerson);
  }
}

Служба проверки личности – это наш пользовательский интерфейс. PersonRepository – это простое Хранилище данных Spring JpaRepository .

Давайте напишем модульный тест с использованием mocks.

class PersonCreateServiceImplMockingTest {

  private final PersonValidateService personValidateService = mock(PersonValidateService.class);
  private final PersonRepository personRepository = mock(PersonRepository.class);
  private final PersonCreateService service = new PersonCreateServiceImpl(personValidateService,
      personRepository);

  @Test
  void shouldFailUserCreation() {
    final var firstName = "Jack";
    final var lastName = "Black";
    doThrow(new ValidationFailedException(""))
        .when(personValidateService)
        .checkUserCreation(firstName, lastName);
    assertThrows(
      ValidationFailedException.class, 
      () -> service.createPerson(firstName, lastName)
    );
  }
}

Ладно, это было довольно просто. Давайте подумаем о чем-то более сложном. Что делать, если создание пользователя пройдет успешно? Это требует немного большей решимости.

Во-первых, нам нужно издеваться Персональное представительство таким образом, saveAndFlush возвращает новый экземпляр Person с заполненным полем id . Во-вторых, нам нужно проверить, что результат PersonDTO содержит ожидаемую информацию.

class PersonCreateServiceImplMockingTest {
  // initialization... 

  @Test
  void shouldCreateNewUser() {
    final var firstName = "Lisa";
    final var lastName = "Green";
    when(personRepository.saveAndFlush(any()))
        .thenAnswer(invocation -> {
          Person person = invocation.getArgument(0);
          assert Objects.equals(person.getFirstName(), firstName);
          assert Objects.equals(person.getLastName(), lastName);
          return person.setId(1L);
        });
    final var personDTO = service.createPerson(firstName, lastName);
    assertEquals(personDTO.getFirstName(), firstName);
    assertEquals(personDTO.getLastName(), lastName);
    assertNotNull(personDTO.getId());
  }
}

Это стало непросто. Но пока нет времени отдыхать. Предположим, что Служба создания пользователя была расширена с помощью метода создать семью .

@Service
@RequiredArgsConstructor
public class PersonCreateServiceImpl implements PersonCreateService {

  private final PersonValidateService personValidateService;
  private final PersonRepository personRepository;

  @Override
  @Transactional
  public List createFamily(Iterable firstNames, String lastName) {
    final var people = new ArrayList();
    firstNames.forEach(firstName -> people.add(createPerson(firstName, lastName)));
    return people;
  }

  @Override
  @Transactional
  public PersonDTO createPerson(String firstName, String lastName) {
    personValidateService.checkUserCreation(firstName, lastName);
    final var createdPerson = personRepository.saveAndFlush(
        new Person()
            .setFirstName(firstName)
            .setLastName(lastName)
    );
    return DTOConverters.toPersonDTO(createdPerson);
  }
}

Это тоже нуждается в тестах. Давайте попробуем написать один из них.

class PersonCreateServiceImplMockingTest {
  // initialization...

  @Test
  void shouldCreateFamily() {
    final var firstNames = List.of("John", "Samantha", "Kyle");
    final var lastName = "Purple";
    final var idHolder = new AtomicLong(0);
    when(personRepository.saveAndFlush(any()))
        .thenAnswer(invocation -> {
          Person person = invocation.getArgument(0);
          assert firstNames.contains(person.getFirstName());
          assert Objects.equals(person.getLastName(), lastName);
          return person.setId(idHolder.incrementAndGet());
        });
    final var people = service.createFamily(firstNames, lastName);
    for (int i = 0; i < people.size(); i++) {
      final var personDTO = people.get(i);
      assertEquals(personDTO.getFirstName(), firstNames.get(i));
      assertEquals(personDTO.getLastName(), lastName);
      assertNotNull(personDTO.getId());
    }
    verify(personValidateService, times(3)).checkUserCreation(any(), any());
    verify(personRepository, times(3)).saveAndFlush(any());
  }

}

Когда я смотрю на этот код, я не вижу ничего, кроме ерунды. Поток данных настолько сложен, что практически невозможно понять, что на самом деле делает тест. Более того, нет тестирования но проверка того, что некоторые конкретные методы вызывались в определенное время. “В чем разница?” вы можете спросить. Представьте, что выполнение метода saveAndFlush было заменено пользовательским, который обновляет сущность и сохраняет предыдущее состояние в таблице архива (например, сохранить С Архивированием ). Хотя бизнес-логика та же, тест не пройдет из-за того, что новый метод не был подвергнут издевательствам.

Возможно, последнее утверждение было недостаточно убедительным. Давайте посмотрим декларацию Человека сущность.

@Entity
@Table(name = "person")
public class Person {

  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  private Long id;

  @Column(name = "first_name")
  private String firstName;

  @Column(name = "last_name")
  private String lastName;

  private ZonedDateTime dateCreated;

  @PrePersist
  void prePersist() {
    dateCreated = ZonedDateTime.now();
  }

  // getters, setters
}

У него есть Предварительный список обратный вызов, который устанавливает дату создания непосредственно перед вставкой новой записи в базу данных. Проблема в том, что его нельзя протестировать с помощью макетов. Логика вызывается поставщиком JPA внутренне. Издевательства просто не могут имитировать такое поведение.

Итак, давайте сделаем выводы. Макеты идеально подходят для тестирования тех функций, которые вы контролируете. Обычно это определяемые пользователем сервисы (например, Служба проверки личности ). Spring Data и JPA генерируют множество данных во время выполнения. Насмешки не помогут вам проверить это.

Уровень обслуживания + База данных H2

Если у вас есть служба, которая должна взаимодействовать с базой данных, единственный способ по-настоящему протестировать ее – запустить на реальном экземпляре БД. H2 ДБ – это первое, что приходит на ум.

К счастью, нам не нужны какие-либо сложные конфигурации и хитрое объявление компонентов для запуска базы данных в тестовой среде. Пружинный ботинок позаботится об этом.

Тест Jpa Данных

С чего мы начнем? Во-первых, нам нужно объявить набор тестов.

@DataJpaTest
class PersonCreateServiceImplDataJpaTest {
  @Autowired
  private PersonRepository personRepository;
  @MockBean
  private PersonValidateService personValidateService;
  @Autowired
  private PersonCreateService personCreateService;
}

@DataJpaTest аннотация здесь творит чудеса. И, в частности, есть 4 пункта.

  1. Запуск встроенного экземпляра базы данных H2.
  2. Создание схемы базы данных в соответствии с объявленными классами сущностей.
  3. Добавление всех компонентов репозиториев в контекст приложения.
  4. Обертывание всего набора тестов аннотацией @Transactional. Таким образом, каждое выполнение теста становится независимым.

Вы, наверное, заметили аннотацию @MockBean . Это функция Spring, которая не только издевается над интерфейсом, но и добавляет его в контекст приложения. Таким образом, он может быть автоматически подключен другими способами во время тестового запуска.

Теперь нам нужно создать экземпляр службы, которую мы собираемся протестировать.

@DataJpaTest
class PersonCreateServiceImplDataJpaTest {

  @Autowired
  private PersonRepository personRepository;
  @MockBean
  private PersonValidateService personValidateService;
  @Autowired
  private PersonCreateService personCreateService;

  @TestConfiguration
  static class TestConfig {

    @Bean
    public PersonCreateService personCreateService(
        PersonRepository personRepository,
        PersonValidateService personValidateService
    ) {
      return new PersonCreateServiceImpl(
          personValidateService, personRepository
      );
    }
  }
}

На мой взгляд, наиболее гибкое решение обеспечивает аннотацию @TestConfiguration . Это позволяет нам изменять существующий контекст приложения. Когда добавляется Служба создания пользователя , ее можно легко ввести с помощью @Autowired .

Хорошо, давайте начнем с простого теста счастливого пути метода создать семью .

@DataJpaTest
class PersonCreateServiceImplDataJpaTest {
  // initialization...

  @Test
  void shouldCreateOnePerson() {
    final var people = personCreateService.createFamily(
        List.of("Simon"),
        "Kirekov"
    );
    assertEquals(1, people.size());
    final var person = people.get(0);
    assertEquals("Simon", person.getFirstName());
    assertEquals("Kirekov", person.getLastName());
    assertTrue(person.getDateCreated().isBefore(ZonedDateTime.now()));
  }
}

Как вы можете видеть, этот тест намного чище, короче и понятнее, чем тест с использованием mocks. Более того, теперь мы можем тестировать обратные вызовы в режиме гибернации (например, @prePersist ).

Ну, а этот был настоящим тортом. Но что, если произойдет исключение ValidationFailedException ? Это означает, что транзакция должна быть отменена. Давайте выясним это.

@DataJpaTest
class PersonCreateServiceImplDataJpaTest {
  // initialization...

  @Test
  void shouldRollbackIfAnyUserIsNotValidated() {
    doThrow(new ValidationFailedException(""))
        .when(personValidateService)
        .checkUserCreation("John", "Brown");
    assertThrows(ValidationFailedException.class, () -> personCreateService.createFamily(
        List.of("Matilda", "Vasya", "John"),
        "Brown"
    ));
    assertEquals(0, personRepository.count());
  }
}

Выполнение должно завершиться неудачей при создании "John" . Это означает, что общее количество людей должно быть равно 0 потому что выброс исключения откатывает транзакцию.

expected: <0> but was: <2>
Expected :0
Actual   :2

Что-то пошло не так. Похоже, что транзакция по какой-то причине не была отменена. И это правда.

Я уже упоминал, что @DataJpaTest завершает набор @Транзакционным . Таким образом, набор тестов и сервис являются транзакционными. Уровень распространения аннотации по умолчанию равен ТРЕБУЕТСЯ . Это означает, что вызов другого транзакционного метода не запускает новую транзакцию. Вместо этого он продолжает выполнять инструкции SQL в текущем. |/Исключение ValidationFailedException происходящее не отменяет транзакцию, поскольку исключение не выходит за ее рамки. Так, граф возвращается 2 вместо того, чтобы 0 .

Я описал это явление в своей статье “Весенние данные — транзакционные предостережения”.

Что мы можем с этим поделать? Мы могли бы пометить Сервис создания человека.создать семью распространение транзакций как REQUIRES_NEW. Это решает проблему с текущим тестом, но добавляет новые. Вы можете найти больше примеров в репозитории, который я отметил в начале статьи.

Если @DataJpaTest вызывает такие странные проблемы, то в чем его цель? Что ж, его название описывает цель. Его следует использовать с тестами репозитория .

public interface PersonRepository extends JpaRepository {

  @Query("select distinct p.lastName from Person p")
  Set findAllLastNames();
}
@DataJpaTest
class PersonRepositoryDataJpaTest {

  @Autowired
  private PersonRepository personRepository;

  @Test
  void shouldReturnAlLastNames() {
    personRepository.saveAndFlush(new Person().setFirstName("John").setLastName("Brown"));
    personRepository.saveAndFlush(new Person().setFirstName("Kyle").setLastName("Green"));
    personRepository.saveAndFlush(new Person().setFirstName("Paul").setLastName("Brown"));

    assertEquals(Set.of("Brown", "Green"), personRepository.findAllLastNames());
  }
}

Видишь? Это идеально подходит. Тест выполняет один оператор SQL. В этом случае транзакционное поведение по умолчанию @DataJpaTest становится удобным. Но уровни обслуживания гораздо сложнее. И для этого нам нужен другой инструмент.

Испытание пружинного ботинка

Давайте немного перепишем тестовое объявление.

@SpringBootTest(webEnvironment = WebEnvironment.NONE)
@AutoConfigureTestDatabase
class PersonCreateServiceImplSpringBootTest {

  @Autowired
  private PersonRepository personRepository;
  @MockBean
  private PersonValidateService personValidateService;
  @Autowired
  private PersonCreateService personCreateService;

  @BeforeEach
  void init() {
    personRepository.deleteAll();
  }
}

Существуют некоторые различия с альтернативой @DataJpaTest .

Аннотация @SpringBootTest запускает весь контекст Spring, а не только репозитории JPA. Еще одна важная вещь заключается в том, что он не заключает набор тестов в @Транзакционный .Тот веб-среда. Параметр NONE

Аннотация @AutoConfigureTestDatabase настраивает встроенную базу данных H2 и создает схему в соответствии с определенными сущностями. @DataJpaTest уже включает его, поэтому объявлять их оба излишне (если только мы не хотим параметризовать @AutoConfigureTestDatabase но это выходит за рамки).

Возможно, вы также заметили, что мы просто автоматически подключили Службу создания пользователей без каких-либо дополнительных настроек. В связи с тем, что @Springboost создает экземпляр каждого компонента по умолчанию, служба уже присутствует в контексте приложения.

Сброс базы данных в @beforeEach обратный вызов необходим, так как @SpringBootTest не обеспечивает транзакционное поведение. Но нам нужно поддерживать чистоту таблицы между запусками тестов.

Итак, давайте проведем тесты из примера @DataJpaTest и посмотрим, как это работает.

@SpringBootTest(webEnvironment = WebEnvironment.NONE)
@AutoConfigureTestDatabase
class PersonCreateServiceImplSpringBootTest {

  @Autowired
  private PersonRepository personRepository;
  @MockBean
  private PersonValidateService personValidateService;
  @Autowired
  private PersonCreateService personCreateService;

  @BeforeEach
  void init() {
    personRepository.deleteAll();
  }

  @Test
  void shouldCreateOnePerson() {
    final var people = personCreateService.createFamily(
        List.of("Simon"),
        "Kirekov"
    );
    assertEquals(1, people.size());
    final var person = people.get(0);
    assertEquals("Simon", person.getFirstName());
    assertEquals("Kirekov", person.getLastName());
    assertTrue(person.getDateCreated().isBefore(ZonedDateTime.now()));
  }

  @Test
  void shouldRollbackIfAnyUserIsNotValidated() {
    doThrow(new ValidationFailedException(""))
        .when(personValidateService)
        .checkUserCreation("John", "Brown");
    assertThrows(ValidationFailedException.class, () -> personCreateService.createFamily(
        List.of("Matilda", "Vasya", "John"),
        "Brown"
    ));
    assertEquals(0, personRepository.count());
  }
}

Все работает как по волшебству.

Во всех наших тестовых примерах предполагалось, что Person Validate Service.check Создание пользователя имеет простую логику проверки входных параметров. Но на самом деле это может быть неправдой. Служба также может взаимодействовать с базой данных для проверки предварительных условий. Итак, давайте подражать поведению.

Предположим, что валидатор не позволяет создавать нового человека, если есть человек с той же фамилией. Чтобы протестировать этот сценарий, нам нужно правильно смоделировать службу проверки личности и заранее вставить члена семьи, прежде чем вызывать метод PersonCreateService.createFamily .

@SpringBootTest(webEnvironment = WebEnvironment.NONE)
@AutoConfigureTestDatabase
class PersonCreateServiceImplSpringBootTest {
  // initialization...

  @Test
  void shouldRollbackIfOneUserIsNotValidated() {
    doAnswer(invocation -> {
      final String lastName = invocation.getArgument(1);
      final var exists = personRepository.exists(Example.of(new Person().setLastName(lastName)));
      System.out.println("Person with " + lastName + " exists: " + exists);
      if (exists) {
        throw new ValidationFailedException("Person with " + lastName + " already exists");
      }
      return null;
    }).when(personValidateService).checkUserCreation(any(), any());

    personRepository.saveAndFlush(new Person().setFirstName("Alice").setLastName("Purple"));

    assertThrows(ValidationFailedException.class, () -> personCreateService.createFamily(
        List.of("Matilda"),
        "Purple"
    ));
    assertEquals(1, personRepository.count());
  }
}

Это работает!

Кстати, вы также можете запустить @DataJpaTest пакеты без транзакций . Вам просто нужно @Транзакционная(распространение) аннотация.

Вывод

Спасибо вам за чтение! Это довольно длинная статья, и я рад, что вы ее прочитали. В следующий раз мы обсудим интеграцию тестовых контейнеров с Spring Test. Если у вас есть какие-либо вопросы или предложения, пожалуйста, оставьте свои комментарии ниже. Увидимся в следующий раз!

Оригинал: “https://dev.to/kirekov/spring-boot-testing-data-and-services-288f”