Как стать автором
Обновить

Тесты на дженериках: пишем кода в 3 раза меньше. Параметризация AssertJ и сравнение Json через объекты

Уровень сложностиСредний
Время на прочтение4 мин
Количество просмотров3.7K

Продолжаю серию публикаций про наши Java-онлайн курсы. Предыдущие посты:

Сразу предупрежу: точно так же, как в контроллерах на дженериках сами контроллеры не параметризируются, здесь мы НЕ БУДЕМ параметризировать сами классы тестов. Поэтому не спешите писать комментарии, не прочитав статьи, что это «Bad practice». По поводу усложнения кода заранее отвечу так же, как и в комментариях к статье про контроллеры — код тестов и их написание становятся проще, за счет усложнения инструментов (собственно на этом и строится разработка фреймворков и ООП). Можно считать приведенные здесь подходы слоем абстракции, праметризирующий подход популярной библиотеки AssertJ к сравнению объектов и расширяющий его на сравнение json объектов.

Тесты сервисов/репозиториев

Большинство разработчиков, кто писал тесты, видели/использовали такие привычные конструкции при тестировании сервисов/репозиториев:

@Test
void getFromService() {
    User actual = service.get(USER_ID);
    assertEquals(USER_ID, actual.getId());
    assertEquals(user.getName(), actual.getName());
    assertEquals(user.getEmail(), actual.getEmail());
    assertEquals(user.getCaloriesPerDay(), actual.getCaloriesPerDay());
    assertEquals(user.getRoles(), actual.getRoles());
}

и похожие на них для проверки возвращаемых из контроллера json объектов c помощью MockMvcResultMatchers.jsonPath (ссылка на полный код класса внизу статьи)

@Test
void getFromController() throws Exception {
    mockMvc.perform(MockMvcRequestBuilders.get(REST_URL + USER_ID)
                    .with(httpBasic(admin.getEmail(), admin.getPassword())))
            .andDo(print())
            .andExpect(status().isOk())
            .andExpect(content().contentTypeCompatibleWith(MediaType.APPLICATION_JSON))
            .andExpect(jsonPath("$.id").value(USER_ID))
            .andExpect(jsonPath("$.name").value(user.getName()))
            .andExpect(jsonPath("$.email").value(user.getEmail()))
            .andExpect(jsonPath("$.caloriesPerDay").value(user.getCaloriesPerDay()))
            .andExpect(jsonPath("$.roles", hasSize(1)))
            .andExpect(jsonPath("$.roles", contains(Role.USER.name())));
}

И это для небольшого объекта без вложений и с одним элементом в колелкции ролей! Понятно, что часто на тестирование ВСЕХ полей реальных объекта забивается.

Давайте попробуем упростить жизнь разработчику, пройдя путь, который мы используем уже почти 10 лет на нашей стажировке TopJava (Maven/ Spring/ Security/ Spring Boot/ JPA(Hibernate)/ Swagger/OpenAPI 3.0)/ Rest)

  1. Если возвращается объект DTO, то тесты можно упростить, переопределив в нем equals/hasCode (например через Lombok @EqualsAndHashCode(callSuper = true))

    @Test
    void getToFromService() {
        UserTo actualTo = service.getTo(USER_ID);
        assertEquals(userTo, actualTo);
    }
    
  2. Для контроллеров все немного сложнее: нам нужен утильный класс для преобразования Json в объект, который мы затем можем сравнить:

    public class JsonUtil {
        public static <T> T readValue(String json, Class<T> clazz) throws IOException {
            return getObjectMapper().readValue(json, clazz);
        }
        ...
    }
    @Test
    void getToFromController() throws Exception {
        MvcResult result = mockMvc.perform(MockMvcRequestBuilders.get(REST_URL_TO + USER_ID)
           .with(httpBasic(admin.getEmail(), admin.getPassword())))
           .andExpect(...)
           .andReturn();
        String json = result.getResponse().getContentAsString();
        User actual = JsonUtil.readValue(json, User.class);
        assertEquals(user, actual);
    }
    

    Или можно сократить:

    @Test
    void getToFromController() throws Exception {
        mockMvc.perform(MockMvcRequestBuilders.get(REST_URL_TO + USER_ID)
           .with(httpBasic(admin.getEmail(), admin.getPassword())))
           .andExpect(...)
           .andExpect(result -> assertEquals(user,
                JsonUtil.readValue(result.getResponse().getContentAsString(), User.class)));
    }
    
  3. Если объект является Entity, и переопределить equals/hasCode по всем полям мы не можем, можно упростить код с помощью мощной библиотеки AssertJ.
    Будте внимательны: в Junit методах проверки порядок аргументов (expected, actual), а в методах AssertJ наоборот (actual,expected).

    @Test
    void getEntityFromService() {
        User actual = service.get(USER_ID);
        assertThat(actual).usingRecursiveComparison().ignoringFields("registered", "password").isEqualTo(user);
    }
    

    Чтобы не дублировать стратегию сравнения, можно вынсти ее в константу и использовать во всех тестах:

    public static BiConsumer<User, User> USER_MATCHER = 
          (a, e) -> assertThat(a).usingRecursiveComparison().ignoringFields("registered", "password").isEqualTo(e)
      
    @Test
    void getEntityFromService() {
        User actual = service.get(USER_ID);
        USER_MATCHER.accept(actual, user);
    }
  4. Применяем этот подход к контроллерам:

    @Test
    void getEntityFromController() {
        mockMvc.perform(MockMvcRequestBuilders.get(REST_URL_TO + USER_ID)
           .with(httpBasic(admin.getEmail(), admin.getPassword())))
           .andExpect(...)
           .andExpect(result -> USER_MATCHER.accept(
                    JsonUtil.readValue(result.getResponse().getContentAsString(), User.class), user));
    }
    
  5. Осталось расширить наш подход на все случаи:

В некоторых сложных случаях, когда мы используем JsonUtil для создания json тела POST запроса, а поля объекта помечены как @JsonIgnore или @JsonProperty(access = Access.WRITE_ONLY), приходится использовать хак - добавлять эти поля вручную (код JsonUtil.writeAdditionProps и, например, использование в AdminUserControllerTest.update)

Если проект большой и тестов много, мы получаем выигрыш даже не 3-X, а на порядок!
При этом уходит все дублирование и ошибок также становится на порядок меньше.

Напоследок традиционно: приятного кодирования и приглашаем на наши курсы.

Теги:
Хабы:
Всего голосов 8: ↑7 и ↓1+8
Комментарии3

Публикации

Истории

Работа

Java разработчик
341 вакансия

Ближайшие события