Главная страница
Навигация по странице:

  • Используйте -noverify -XX:TieredStopAtLevel=1

  • Избегайте использовать assertTrue() и assertFalse()

  • Используйте параметризованные тесты

  • Читаемые названия тестов при помощи @DisplayName или обратных кавычек в Kotlin

  • Имитируйте внешние сервисы

  • Используйте Awaitility для тестирования асинхронного кода

  • Не надо резолвить DI-зависимости (Spring)

  • Не используйте статический доступ. Никогда

  • Используйте внедрение в конструктор

  • Не используйте Instant.now() или new Date()

  • Разделяйте асинхронное выполнение и собственно логику

  • Basic Java Tools for Building & Testing Apps


    Скачать 4.76 Mb.
    НазваниеBasic Java Tools for Building & Testing Apps
    АнкорJUNIt
    Дата14.11.2022
    Размер4.76 Mb.
    Формат файлаpdf
    Имя файла5_2_0_Maven_JUnit_Tutorial.pdf
    ТипРеферат
    #787190
    страница16 из 16
    1   ...   8   9   10   11   12   13   14   15   16
    Не используйте in-memory-базы данных для тестов
    С in-memory базой вы тестируете не в той среде, где будет работать ваш код
    Используя in-memory базу (
    H2
    ,
    HSQLDB
    ,
    Fongo
    ) для тестов, вы жертвуете их достоверностью и рамками применимости. Такие базы данных часто ведут себя иначе и выдают отличающиеся результаты. Такой тест может пройти успешно, но не гарантирует корректной работы приложения на проде. Более того, вы можете запросто оказаться в ситуации, когда вы не можете использовать или протестировать какое-то характерное для вашей базы поведение или фичу, потому в in-memory БД они не реализованы или ведут себя иначе.
    Решение: использовать такую же БД, как и в реальной эксплуатации. Замечательная библиотека
    Testcontainers предоставляет богатый API для Java-приложений, позволяющий управлять контейнерами прямо из кода тестов.
    Java/JVM
    Используйте
    -noverify -XX:TieredStopAtLevel=1
    Всегда добавляйте опции
    JVM -noverify -XX:TieredStopAtLevel=1
    в вашу конфигурацию для запуска тестов. Это сэкономит вам 1-2 секунды на старте виртуальной машины перед тем, как начнётся выполнение тестов. Это особенно полезно на начальной стадии работы над тестами, когда вы часто запускаете их из IDE.
    Обратите внимание, что начиная с Java 13
    -noverify объявлен устаревшим.
    Совет: добавьте эти аргументы к шаблону конфигурации “JUnit” в IntelliJ IDEA, чтобы не делать это каждый раз при создании нового проекта.

    Используйте AssertJ
    AssertJ
    — исключительно мощная и зрелая библиотека, обладающая развитым и безопасным API, а также большим набором функций проверки значений и информативных сообщений об ошибках тестирования. Множество удобных функций проверки избавляет программиста от необходимости описывать комплексную логику в теле тестов, позволяя делать тесты лаконичными. Например:
    assertThat(actualProduct)
    .isEqualToIgnoringGivenFields(expectedProduct,
    "id"
    );
    assertThat(actualProductList).containsExactly(
    createProductDTO(
    "1"
    ,
    "Smartphone"
    ,
    250.00
    ),
    createProductDTO(
    "1"
    ,
    "Smartphone"
    ,
    250.00
    )
    );
    assertThat(actualProductList)
    .usingElementComparatorIgnoringFields(
    "id"
    )
    .containsExactly(expectedProduct1, expectedProduct2);
    assertThat(actualProductList)
    .extracting(Product::getId)
    .containsExactly(
    "1"
    ,
    "2"
    );
    assertThat(actualProductList)
    .anySatisfy(product -> assertThat(product.getDateCreated()).isBetween(inst ant1, instant2));
    assertThat(actualProductList)
    .filteredOn(product -> product.getCategory().equals(
    "Smartphone"
    ))
    .allSatisfy(product -> assertThat(product.isLiked()).isTrue());
    Избегайте использовать
    assertTrue()
    и
    assertFalse()
    Использование простых assertTrue()
    или assertFalse()
    приводит к загадочным сообщениям об ошибках тестов:
    // Неправильно
    assertTrue(actualProductList.contains(expectedProduct));
    assertTrue(actualProductList.size() ==
    5
    );
    assertTrue(actualProduct instanceof
    Product);
    expected: <
    true
    > but was: <
    false
    >
    Используйте вместо них вызовы AssertJ, которые «из коробки» возвращают понятные и информативные сообщения.
    // Правильно
    assertThat(actualProductList).contains(expectedProduct);
    assertThat(actualProductList).hasSize(
    5
    );
    assertThat(actualProduct).isInstanceOf(Product.class);
    Expecting:
    <[Product[id=
    1
    , name=
    'Samsung Galaxy'
    ]]>
    to contain:
    <[Product[id=
    2
    , name=
    'iPhone'
    ]]>
    but could not find:
    <[Product[id=
    2
    , name=
    'iPhone'
    ]]>
    Если вам надо проверить boolean-значение, сделайте сообщение более информативным при помощи метода as()
    AssertJ.
    Используйте JUnit5
    JUnit5
    — превосходная библиотека для (юнит-)тестирования. Она находится в процессе постоянного развития и предоставляет программисту множество полезных возможностей, таких, например, как параметризованные тесты, группировки, условные тесты, контроль жизненного цикла.

    Используйте параметризованные тесты
    Параметризованные тесты позволяют запускать один и тот же тест с набором различных входных значений. Это позволяет проверять несколько кейсов без написания лишнего кода. В JUnit5 для этого есть отличные инструменты
    @ValueSource
    ,
    @EnumSource
    ,
    @CsvSource и
    @MethodSource
    // Правильно
    @ParameterizedTest
    @ValueSource(strings = ["§ed2d", "sdf_", "123123", "§_sdf__dfww!"])
    public void rejectedInvalidTokens
    (String invalidToken) {
    client.perform(get(
    "/products"
    ).param(
    "token"
    , invalidToken))
    .andExpect(status().is(
    400
    ))
    }
    @ParameterizedTest
    @EnumSource(WorkflowState::class, mode = EnumSource.Mode.INCLUDE, names = ["FAILED
    ", "SUCCEEDED"])
    public void dontProcessWorkflowInCaseOfAFinalState
    (WorkflowState itemsInitialState
    ) {
    // ...
    }
    Я горячо рекомендую использовать этот приём по максимуму, поскольку он позволяет тестировать больше кейсов с минимальными трудозатратами.
    Наконец, я хочу обратить ваше внимание на
    @CsvSource и
    @MethodSource
    , которые можно использовать для более сложной параметризации, где также надо контролировать результат: вы можете передать его в одном из параметров.

    @ParameterizedTest
    @CsvSource({
    "1, 1, 2",
    "5, 3, 8",
    "10, -20, -10"
    })
    public void add
    (
    int summand1, int summand2, int expectedSum) {
    assertThat(calculator.add(summand1, summand2)).isEqualTo(expectedSum);
    }
    @MethodSource особенно эффективен в связке с отдельным тестовым объектом, содержащим все нужные параметры и ожидаемые результаты. К сожалению, в Java описание таких структур данных (т.н. POJO) очень громоздки. Поэтому я приведу пример с использованием дата-классов
    Kotlin. data class
    TestData
    (
    val input: String?,
    val expected: Token?
    )
    @ParameterizedTest
    @
    MethodSource
    (
    "validTokenProvider"
    )
    fun `parse valid tokens`(data: TestData) {
    assertThat(parse(data.input)).isEqualTo(data.expected)
    }
    private fun validTokenProvider
    () = Stream.of(
    TestData(input =
    "1511443755_2"
    , expected = Token(
    1511443755
    ,
    "2"
    )),

    TestData(input =
    "151175_13521"
    , expected = Token(
    151175
    ,
    "13521"
    )),
    TestData(input =
    "151144375_id"
    , expected = Token(
    151144375
    ,
    "id"
    )),
    TestData(input =
    "15114437599_1"
    , expected = Token(
    15114437599
    ,
    "1"
    )),
    TestData(input = null
    , expected = null
    )
    )
    Группируйте тесты
    Аннотация
    @Nested из JUnit5 удобна для группировки тестовых методов. Логически имеет смысл группировать вместе определённые типы тестов (типа
    InputIsXY
    ,
    ErrorCases
    ) или собрать в свою группу методы каждого теста (
    GetDesign и
    UpdateDesign
    ).
    public class
    DesignControllerTest
    {
    @Nested class
    GetDesigns
    {
    @Test void allFieldsAreIncluded
    () {}
    @Test void limitParameter
    () {}
    @Test void filterParameter
    () {}
    }
    @Nested class
    DeleteDesign
    {

    @Test void designIsRemovedFromDb
    () {}
    @Test void return404OnInvalidIdParameter
    () {}
    @Test void return401IfNotAuthorized
    () {}
    }
    }
    Читаемые названия тестов при помощи
    @DisplayName
    или обратных
    кавычек в Kotlin
    В Java можно использовать аннотацию
    @DisplayName
    , чтобы дать тестам более читаемые названия.
    public class
    DisplayNameTest
    {
    @Test

    @DisplayName("Design is removed from database")
    void designIsRemoved
    () {}
    @Test
    @DisplayName("Return 404 in case of an invalid parameter")
    void return404
    () {}
    @Test
    @DisplayName("Return 401 if the request is not authorized")
    void return401
    () {}
    }
    В Kotlin можно использовать имена функций с пробелами внутри, если заключить их в обратные одиночные кавычки. Так вы получите читаемость результатов без избыточности кода.
    @Test fun
    `design is removed from db`() {}
    Имитируйте внешние сервисы
    Для тестирования HTTP-клиентов нам необходимо имитировать сервисы, к которым они обращаются. Я часто использую в этих целях
    MockWebServer из OkHttp. Альтернативами могут служить
    WireMock или
    Mockserver из Testcontainers
    MockWebServer serviceMock = new
    MockWebServer();
    serviceMock.start();
    HttpUrl baseUrl = serviceMock.url(
    "/v1/"
    );
    ProductClient client = new
    ProductClient(baseUrl.host(), baseUrl.port());
    serviceMock.enqueue(
    new
    MockResponse()
    .addHeader(
    "Content-Type"
    ,
    "application/json"
    )
    .setBody(
    "{\"name\": \"Smartphone\"}"
    ));
    ProductDTO productDTO = client.retrieveProduct(
    "1"
    );
    assertThat(productDTO.getName()).isEqualTo(
    "Smartphone"
    );
    Используйте Awaitility для тестирования асинхронного кода
    Awaitility
    — это библиотека для тестирования асинхронного кода. Вы можете указать, сколько раз надо повторять попытки проверки результата перед тем, как признать тест неудачным.
    private static final
    ConditionFactory WAIT = await()
    .atMost(Duration.ofSeconds(
    6
    ))
    .pollInterval(Duration.ofSeconds(
    1
    ))
    .pollDelay(Duration.ofSeconds(
    1
    ));
    @Test public void waitAndPoll
    (){
    triggerAsyncEvent();
    WAIT.untilAsserted(() -> {
    assertThat(findInDatabase(
    1
    ).getState()).isEqualTo(State.SUCCESS);
    });
    }
    Не надо резолвить DI-зависимости (Spring)
    Инициализация DI-фреймворка занимает несколько секунд перед тем, как тесты могут стартовать.
    Это замедляет цикл обратной связи, особенно на начальном этапе разработки.
    Поэтому я стараюсь не использовать DI в интеграционных тестах, а создаю нужные объекты вручную и «провязываю» их между собой. Если вы используете внедрение в конструктор, то это самое простое. Как правило, в своих тестах вы проверяете бизнес-логику, а для этого DI не нужно.
    Более того, начиная с версии 2.2, Spring Boot поддерживает ленивую инициализацию бинов, что заметно ускоряет тесты, использующие DI.
    Ваш код должен быть тестируемым
    Не используйте статический доступ. Никогда
    Статический доступ — это антипаттерн. Во-первых, он запутывает зависимости и побочные эффекты, делая весь код сложночитаемым и подверженным неочевидным ошибкам. Во-вторых, статический доступ мешает тестированию. Вы больше не можете заменять объекты, но в тестах вам нужно использовать моки или реальные объекты с другой конфигурацией (например, DAO- объект, указывающий на тестовую базу данных).
    Вместо статического доступа к коду положите его в нестатический метод, создайте экземпляр класса и передайте полученный объект в конструктор.
    // Неправильно
    public class
    ProductController
    {
    public
    List getProducts
    () {
    List products = ProductDAO.getProducts();
    return mapToDTOs(products);
    }
    }

    // Правильно
    public class
    ProductController
    {
    private
    ProductDAO dao;
    public
    ProductController
    (ProductDAO dao) {
    this
    .dao = dao;
    }
    public
    List getProducts
    () {
    List products = dao.getProducts();
    return mapToDTOs(products);
    }
    }
    К счастью, DI-фреймворки типа Spring предоставляют инструменты, делающие статический доступ ненужным, автоматически создавая и связывая объекты без нашего участия.
    Параметризуйте
    Все релевантные части класса должны иметь возможность настройки со стороны теста. Такие настройки можно передавать в конструктор класса.
    Представьте, например, что ваш DAO имеет фиксированный лимит в 1000 объектов на запрос.
    Чтобы проверить этот лимит, вам надо будет перед тестом добавить в тестовую БД 1001 объект.
    Используя аргумент конструктора, вы можете сделать это значение настраиваемым: в продакшене оставить 1000, в тестировании сократить до 2. Таким образом, чтобы проверить работу лимита вам будет достаточно добавить в тестовую БД всего 3 записи.
    Используйте внедрение в конструктор
    Внедрение полей — зло, оно ведёт к плохой тестируемости кода. Вам необходимо инициализировать DI перед тестами или заниматься стрёмной магией рефлексий. Поэтому предпочтительно использовать внедрение через конструктор, чтобы легко контролировать зависимые объекты в процессе тестирования.
    На Java придётся написать немного лишнего кода:

    // Правильно
    public class
    ProductController
    {
    private
    ProductDAO dao;
    private
    TaxClient client;
    public
    ProductController
    (ProductDAO dao, TaxClient client) {
    this
    .dao = dao;
    this
    .client = client;
    }
    }
    В Kotlin тоже самое пишется намного лаконичнее:
    // Правильно
    class
    ProductController
    (
    private val dao: ProductDAO,
    private val client: TaxClient
    ){
    }
    Не используйте
    Instant.now()
    или
    new Date()
    Не надо получать текущее время вызовами
    Instant.now()
    или new Date()
    в продакшн-коде, если вы хотите тестировать это поведение.

    // Неправильно
    public class
    ProductDAO
    {
    public void updateDateModified
    (String productId) {
    Instant now = Instant.now();
    // !
    Update update = Update()
    .set(
    "dateModified"
    , now);
    Query query = Query()
    .addCriteria(where(
    "_id"
    ).eq(productId));
    return mongoTemplate.updateOne(query, update, ProductEntity.class);
    }
    }
    Проблема в том, что полученное время не может контролироваться со стороны теста. Вы не сможете сравнить полученный результат с конкретным значением, потому что он всё время разный. Вместо этого используйте класс
    Clock из Java.
    // Правильно
    public class
    ProductDAO
    {
    private
    Clock clock;
    public
    ProductDAO
    (Clock clock) {
    this
    .clock = clock;
    }
    public void updateProductState
    (String productId, State state) {
    Instant now = clock.instant();

    // ...
    }
    }
    В этом тесте вы можете создать мок-объект для
    Clock
    , передать его в
    ProductDAO
    и сконфигурировать мок-объект так, чтобы он возвращал одно и то же время. После вызовы updateProductState()
    мы сможем проверить, что в базу данных попало именно заданное нами значение.
    Разделяйте асинхронное выполнение и собственно логику
    Тестирование асинхронного кода — непростая штука. Библиотеки типа Awaitility оказывают большую помощь, но процесс всё равно запутан, и мы можем получить «моргающий» тест. Есть смысл разделять бизнес-логику (обычно синхронную) и асинхронный инфраструктурный код, если такая возможность имеется.
    Например, вынеся бизнес-логику в ProductController мы сможем запросто протестировать её синхронно. Вся асинхронная и параллельная логика останутся в ProductScheduler, который можно протестировать изолированно.
    // Правильно
    public class
    ProductScheduler
    {
    private
    ProductController controller;
    @Scheduled public void start
    () {
    CompletableFuture usFuture = CompletableFuture.supplyAsync(() -> c ontroller.doBusinessLogic(Locale.US));
    CompletableFuture germanyFuture = CompletableFuture.supplyAsync(()
    -> controller.doBusinessLogic(Locale.GERMANY));
    String usResult = usFuture.get();
    String germanyResult = germanyFuture.get();

    }
    }
    1   ...   8   9   10   11   12   13   14   15   16


    написать администратору сайта