Upgrade to Pro — share decks privately, control downloads, hide ads and more …

JUGNsk Meetup #5. Кирилл Кармакулов: "End-to-End тесты на Spring Boot: трудно ли даются и что дают?"

jugnsk
December 15, 2018

JUGNsk Meetup #5. Кирилл Кармакулов: "End-to-End тесты на Spring Boot: трудно ли даются и что дают?"

В докладе:
-как пройти путь от "нет тестов" до "покрытие - 90%" без титанических усилий: за 20% от кода приложения и 50% от времени разработки
-какие подходы оправдали себя, какие доставили боль и страдание,
какие инструменты "будут нам в помощь"
-что было бы хорошо, но на практике - дорого

Доклад даст слушателям набор инструментов и средств для каждодневной работы.

jugnsk

December 15, 2018
Tweet

More Decks by jugnsk

Other Decks in Programming

Transcript

  1. Кирилл Кармакулов Tech-Lead в Improve Group • 20 лет в

    разработке ПО • 4 года дружбы со Spring • Обожаю Java-сообщество
  2. Факты • 4 приложения • 950 тестов • 75-90% покрытие

    тестами • 9 месяцев • ~3 разработчика 3
  3. DRY - Don’t Repeat Yourself • Приложение работает как ожидают

    • Рефакторинг - дешевле • Новые фичи не ломают старые ⇒ уверенность 4
  4. Какие бывают тесты • Unit • Integration (Component) • Application

    (Functional, Microservice) • System - вся Инф. Система 5
  5. Не расскажу о... 7 • о том что есть “из

    коробки” в Spring Boot @JpaTest, @WebMvcTest, @SpringBootConfiguration Толкачев К. (и Борисов Е.) “Проклятие Spring Test”
  6. Не расскажу о... 8 • TestContainers Сергей Егоров “TestContainers —

    интеграционное тестирование с Docker” • Других Framework’ах (Arquillian, Cucumber, ...)
  7. 3 типа тестов • тесты для проверки жизненного цикла •

    тесты для валидации входящих DTO • тесты для поиска 11
  8. Тесты жизненного цикла Вначале поднимаем ВСЕ приложение 12 В каждом

    тесте: • отправляем запрос • проверяем ответ • дополнительные проверки
  9. 13 @RunWith(SpringRunner.class) @SpringBootTest(webEnvironment = RANDOM_PORT) public class ClientCreateTest { @LocalServerPort

    private int serverPort; @Autowired private TestRestTemplate testRestTemplate; Поднимаем приложение
  10. 14 @Test public void create() { CreateClientDto dto = new

    CreateClientDto(); dto.setLogin("bsmith"); dto.setFamilyName("Smith"); ResponseEntity<UUID> re = testRestTemplate .postForEntity("http://localhost:" + serverPort + "/clients/create" dto, UUID.class); assertThat(re.getStatusCode()).isEqualTo(CREATED); assertThat(re.getBody()).isNotNull(); ClientDto result = сlientService.getOne(re.getBody()); assertThat(result).isNotNull(); assertThat(result.getFamilyName()) .isEqualTo(dto.getFamilyName()); }
  11. 15 @Test public void create() { CreateClientDto dto = new

    CreateClientDto(); dto.setLogin("bsmith"); dto.setFamilyName("Smith"); ResponseEntity<UUID> re = testRestTemplate .postForEntity("http://localhost:" + serverPort + "/clients/create" dto, UUID.class); assertThat(re.getStatusCode()).isEqualTo(CREATED); assertThat(re.getBody()).isNotNull(); ClientDto result = сlientService.getOne(re.getBody()); assertThat(result).isNotNull(); assertThat(result.getFamilyName()) .isEqualTo(dto.getFamilyName()); }
  12. 16 @Test public void create() { CreateClientDto dto = new

    CreateClientDto(); dto.setLogin("bsmith"); dto.setFamilyName("Smith"); ResponseEntity<UUID> re = testRestTemplate .postForEntity("http://localhost:" + serverPort + "/clients/create" dto, UUID.class); assertThat(re.getStatusCode()).isEqualTo(CREATED); assertThat(re.getBody()).isNotNull(); ClientDto result = сlientService.getOne(re.getBody()); assertThat(result).isNotNull(); assertThat(result.getFamilyName()) .isEqualTo(dto.getFamilyName()); }
  13. 17 @Test public void create() { CreateClientDto dto = loadResource(

    "client.create.json", CreateClientDto.class); ResponseEntity<UUID> re = testRestTemplate .postForEntity("http://localhost:" + serverPort + "/clients/create" dto, UUID.class); assertThat(re.getStatusCode()).isEqualTo(CREATED); assertThat(re.getBody()).isNotNull(); ClientDto result = сlientService.getOne(re.getBody()); assertThat(result).isNotNull(); assertThat(result.getFamilyName()) .isEqualTo(dto.getFamilyName()); }
  14. 18 @Test public void create() { CreateClientDto dto = loadResource(

    "client.create.json", CreateClientDto.class); UUID uuid = perform(dto, HttpMethod.POST, "clients/create", UUID.class, CREATED); ClientDto result = сlientService.getOne(re.getBody()); assertThat(result).isNotNull(); assertThat(result.getFamilyName()) .isEqualTo(dto.getFamilyName()); }
  15. 19 private ClientDto getClient(UUID uuid) { return perform(uuid, HttpMethod.POST, "clients/"

    + uuid, ClientDto.class, OK); } @Test public void create() { CreateClientDto dto = loadResource( "client.create.json", CreateClientDto.class); UUID uuid = perform(dto, HttpMethod.POST, "clients/create", UUID.class, CREATED); ClientDto result = getClient(uuid); assertThat(result).isNotNull(); assertThat(result.getFamilyName()) .isEqualTo(dto.getFamilyName()); }
  16. 20 private ClientDto getClient(UUID uuid) { return perform(uuid, HttpMethod.POST, "clients/"

    + uuid, ClientDto.class, OK); } @Test public void create() { CreateClientDto dto = loadResource( "client.create.json", CreateClientDto.class); UUID uuid = perform(dto, HttpMethod.POST, "clients/create", UUID.class, CREATED); ClientDto result = getClient(uuid); ClientDto exp = loadResource( "client.created.json", ClientDto.class); assertThat(result).isNotNull(); assertThat(result.getFamilyName()) .isEqualTo(dto.getFamilyName());
  17. 21 private ClientDto getClient(UUID uuid) { return perform(uuid, HttpMethod.POST, "clients/"

    + uuid, ClientDto.class, OK); } @Test public void create() { CreateClientDto dto = loadResource( "client.create.json", CreateClientDto.class); UUID uuid = perform(dto, HttpMethod.POST, "clients/create", UUID.class, CREATED); ClientDto result = getClient(uuid); ClientDto exp = loadResource( "client.created.json", ClientDto.class); assertThat(result) .isEqualToIgnoringGivenFields(exp, "uuid"); }
  18. 22 @Test public void get() { ClientDto actual = getClient(

    UUID.fromString("36dc24c7-7cd1-...")); ClientDto expected = loadResource( "client.get.json", ClientDto.class); assertThat(actual).isEqualTo(expected); }
  19. Как загрузить xyz.json? 23 @SpringBootTest(webEnvironment = RANDOM_PORT) public abstract class

    BaseApplicationTest { @Autowired private ObjectMapper objectMapper; @SneakyThrows protected <T> T loadResource( String resourceName, Class<T> _class ) { try (InputStream is = getClass() .getResourceAsStream(resourceName)) { return objectMapper.readValue(is, _class); } }
  20. Чем AssertJ полезнее? 24 • assertThat(actual) .isEqualToIgnoringGivenFields(expected, ”uuid”) • assertThat(a.getCreated())

    .isBetween(testStart, now()) • assertThat(a.getPhones()) .containsExactlyInAnyOrderElementsOf(e.getPhones())
  21. 26

  22. Что мы вынесли из fail’ов • Один тест на одну

    бизнес-операцию • Ветвление бизнес-логики покрываем Unit- тестами 27
  23. 28

  24. Независимые тесты Какие тесты конфликтуют? • Lifycycle-тесты между собой ◦

    независимиые данные • Lifycycle-тесты и валидационные ◦ независимиые данные • Lifycycle-тесты и Search-тесты ◦ адаптивные тесты на поиск 30
  25. 32 @Test public void search() { List<ClientDto> actual = performGet(

    "clients/search?" + args.getQuery(), LIST_DTO_TYPE); actual.forEach(args::accept); assertThat(actual.stream() .filter(c -> c.getName().startsWith("search")) .count()) .isEqualTo(args.getExpectedCount()); } Тест на поиск
  26. 33 @Parameterized.Parameters public static Iterable<TestArguments> init() { return ImmutableList.of( args("login=in111",

    1, ClientDto::getLogin, containsString("in111")), ..., args("email=host111.xz", 1, ClientDto::getEmail, containsString("host111.xz")), ..., args("deleted=false", 2, ClientDto::isDeleted, equalTo(Boolean.FALSE)), ..., Параметры теста (поиск)
  27. 34 @Value @Builder private static class FieldTestArguments<T> { private final

    long expectedCount; private final Function<ClientDto, T> getter; private final Condition<T> condition; private final String query; @Override public void accept(ClientDto clientDto) { assertThat(getter.apply(clientDto)) .is(condition); } } TestArguments
  28. Валидация @Test public void validate() { CreateClientDto dto = loadResource

    ("client.create.json", CreateClientDto.class); args.modifier.accept(dto); List<String> actual = perform(dto, HttpMethod.POST, "clients/create", LIST_OF_STRINGS, BAD_REQUEST); assertThat(actual) .containsExactlyInAnyOrderElementsOf( args.getExpectedErrors()); } 35
  29. @Parameterized.Parameters public static Iterable<ValArguments> init() { return ImmutableList.of( args(dto ->

    dto.setLogin(null), errors("Логин является обязательным полем")), args(dto -> dto.setLogin(" \t\r\n "), errors("Логин должен состоять из 3 и более" + "латинский букв и цифр")), args(dto -> dto.setLogin("login-validate"), errors("Логин уже используется: login-validate")); } 36
  30. Режимы работы с БД • Локальная: удобна для разработки ◦

    + быстро: секунды ◦ ? близка к prod’у ◦ - update-тесты: требуют перенакатить данные ◦ - не подходит для build’а • Embedded Database ◦ - медленнее: 30с - 3м ◦ + любые тесты ◦ + build ◦ - часто не близка к prod’у 39
  31. Embedded Database • Docker + TestContainers • Настоящие embedded: ◦

    otj-pg-embedded для PostgreSQL ▪ embedded-database-spring-test ◦ postgresql-embedded для PostgreSQL ◦ wix-embedded-mysql для MySQL 40
  32. Как поднять • Docker + TestContainers • Настоящие embedded: ◦

    otj-pg-embedded для PostgreSQL ▪ embedded-database-spring-test ◦ postgresql-embedded для PostgreSQL ◦ wix-embedded-mysql для MySQL 41
  33. @TestConfiguration public class TestPersistenceConfig { @Bean public EmbeddedPostgres embeddedPostgres() {

    try { return EmbeddedPostgres.builder().start(); } catch (IOException e) { log.error("Failed to start embedded Postgres"); throw new IllegalArgumentException(e); } } 43
  34. @Primary @Bean public HikariConfig applicationHikariConfig( EmbeddedPostgres embedded ) { HikariConfig

    hikariConfig = new HikariConfig(); String jdbcUrl = embedded.getJdbcUrl( "postgres", "postgres"); log.info("JDBC URL: {}", jdbcUrl); hikariConfig.setJdbcUrl(jdbcUrl); return hikariConfig; } 44
  35. Непересекающиеся данные 45 Надо думать :-( • Данные одного теста

    не должны пересекаться с данными другого теста
  36. Факты • Маленькое приложение ◦ 120 классов, ◦ 154 теста,

    ◦ Тесты: 50 секунд, build: 65 секунд, ◦ 25 тестовых классов ◦ 80% покрытие 46 • Среднее приложение ◦ 332 класса, ◦ 678 тестов, ◦ Тесты: 01:33, build: 02:00, ◦ 108 тестовых классов ◦ 85% покрытие
  37. Рекомендации • Поднимать все приложение один раз ◦ Ветвление -

    в Unit-тесте • Один бизнес-шаг - один тест • Поиск - независимый от тестов ЖК • Валидация - на “хорошем” DTO • Независимые данные • Использовать Embedded БД или аналог 48