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

  • Используйте заданные значения вместо случайных

  • Где можно, используйте вспомогательные функции

  • Не злоупотребляйте переменными

  • Не расширяйте существующие тесты, чтобы «добавить ещё одну маленькую штучку»

  • Проверяйте только то, что хотите протестировать

  • Не прячьте релевантные параметры (во вспомогательных функциях)

  • Держите тестовые данные внутри самих тестов

  • Используйте композицию вместо наследования

  • Не переиспользуйте продакшн-код

  • Не копируйте бизнес-логику в тесты

  • Не пишите слишком много логики

  • Тестируйте максимально полную связку компонентов

  • 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
    страница15 из 16
    1   ...   8   9   10   11   12   13   14   15   16
    Используйте префиксы “actual*” и “expected*”
    // Неправильно
    ProductDTO product1 = requestProduct(
    1
    );
    ProductDTO product2 = new
    ProductDTO(
    "1"
    , List.of(State.ACTIVE, State.REJECTED))
    assertThat(product1).isEqualTo(product2);
    Если вы собираетесь использовать переменные в проверке на совпадение значений, добавьте к этим переменным префиксы “actual” и “expected”. Так вы улучшите читаемость кода и проясните назначение переменных. Кроме того, так их сложнее перепутать при сравнении.
    // Правильно
    ProductDTO actualProduct = requestProduct(
    1
    );
    ProductDTO expectedProduct = new
    ProductDTO(
    "1"
    , List.of(State.ACTIVE, State.REJEC
    TED))
    assertThat(actualProduct).isEqualTo(expectedProduct);
    // ясно и красиво
    Используйте заданные значения вместо случайных
    Избегайте подавать случайные значения на вход тестов. Это может привести к «морганию» тестов, что чертовски сложно отлаживать. Кроме того, увидев в сообщении об ошибке случайное значение, вы не сможете проследить его до того места, где ошибка возникла.
    // Неправильно
    Instant ts1 = Instant.now();
    // 1557582788
    Instant ts2 = ts1.plusSeconds(
    1
    );
    // 1557582789
    int randomAmount = new
    Random().nextInt(
    500
    );
    // 232
    UUID uuid = UUID.randomUUID();
    // d5d1f61b-0a8b-42be-b05a-bd458bb563ad
    Используйте для всего подряд разные заранее заданные значения. Так вы получите идеально воспроизводимые результаты тестов, а также быстро найдёте нужное место в коде по сообщению об ошибке.
    // Правильно
    Instant ts1 = Instant.ofEpochSecond(
    1550000001
    );
    Instant ts2 = Instant.ofEpochSecond(
    1550000002
    );
    int amount =
    50
    ;
    UUID uuid = UUID.fromString(
    "00000000-000-0000-0000-000000000001"
    );
    Вы можете записать это ещё короче, используя вспомогательные функции (см. ниже).
    Пишите краткие и конкретные тесты

    Где можно, используйте вспомогательные функции
    Вычленяйте повторяющийся код в локальные функции и давайте им понятные имена. Так ваши тесты будут компактными и легко читаемыми с первого взгляда.
    // Неправильно
    @Test public void categoryQueryParameter
    () throws
    Exception {
    List products = List.of(
    new
    ProductEntity().setId(
    "1"
    ).setName(
    "Envelope"
    ).setCategory(
    "Office
    "
    ).setDescription(
    "An Envelope"
    ).setStockAmount(
    1
    ),
    new
    ProductEntity().setId(
    "2"
    ).setName(
    "Pen"
    ).setCategory(
    "Office"
    ).se tDescription(
    "A Pen"
    ).setStockAmount(
    1
    ),
    new
    ProductEntity().setId(
    "3"
    ).setName(
    "Notebook"
    ).setCategory(
    "Hardwa re"
    ).setDescription(
    "A Notebook"
    ).setStockAmount(
    2
    )
    );
    for
    (ProductEntity product : products) {
    template.execute(createSqlInsertStatement(product));
    }
    String responseJson = client.perform(get(
    "/products?category=Office"
    ))
    .andExpect(status().is(
    200
    ))
    .andReturn().getResponse().getContentAsString();
    assertThat(toDTOs(responseJson))
    .extracting(ProductDTO::getId)
    .containsOnly(
    "1"
    ,
    "2"
    );
    }

    // Правильно
    @Test public void categoryQueryParameter2
    () throws
    Exception {
    insertIntoDatabase(
    createProductWithCategory(
    "1"
    ,
    "Office"
    ),
    createProductWithCategory(
    "2"
    ,
    "Office"
    ),
    createProductWithCategory(
    "3"
    ,
    "Hardware"
    )
    );
    String responseJson = requestProductsByCategory(
    "Office"
    );
    assertThat(toDTOs(responseJson))
    .extracting(ProductDTO::getId)
    .containsOnly(
    "1"
    ,
    "2"
    );
    }

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

    список параметров переменной длины сделает ваш код ещё изящнее
    (
    ìnsertIntoDatabase()
    )

    вспомогательные функции также можно использовать для создания простых значений. В
    Kotlin это сделано ещё лучше через функции-расширения
    // Правильно (Java)
    Instant ts = toInstant(
    1
    );
    // Instant.ofEpochSecond(1550000001)
    UUID id = toUUID(
    1
    );
    // UUID.fromString("00000000-0000-0000-a000-000000000001")

    // Правильно (Kotlin)
    val ts =
    1
    .toInstant()
    val id =
    1
    .toUUID()
    Вспомогательные функции на Kotlin можно реализовать так: fun
    Int toInstant
    (): Instant = Instant.ofEpochSecond(
    this
    .toLong())
    fun
    Int toUUID
    (): UUID = UUID.fromString(
    "00000000-0000-0000-a000-
    ${this.toString(
    ).padStart(
    11
    ,
    '0'
    )}
    "
    )
    Не злоупотребляйте переменными
    Условный рефлекс у программиста — вынести часто используемые значения в переменные.
    // Неправильно
    @Test public void variables
    () throws
    Exception {
    String relevantCategory =
    "Office"
    ;
    String id1 =
    "4243"
    ;
    String id2 =
    "1123"
    ;
    String id3 =
    "9213"
    ;
    String irrelevantCategory =
    "Hardware"
    ;
    insertIntoDatabase(
    createProductWithCategory(id1, relevantCategory),
    createProductWithCategory(id2, relevantCategory),
    createProductWithCategory(id3, irrelevantCategory)

    );
    String responseJson = requestProductsByCategory(relevantCategory);
    assertThat(toDTOs(responseJson))
    .extracting(ProductDTO::getId)
    .containsOnly(id1, id2);
    }
    Увы, это очень перегружает код. Хуже того, увидев значение в сообщении об ошибке, — его будет невозможно проследить до места, где ошибка возникла.
    «KISS важнее DRY»
    // Правильно
    @Test public void variables
    () throws
    Exception {
    insertIntoDatabase(
    createProductWithCategory(
    "4243"
    ,
    "Office"
    ),
    createProductWithCategory(
    "1123"
    ,
    "Office"
    ),
    createProductWithCategory(
    "9213"
    ,
    "Hardware"
    )
    );
    String responseJson = requestProductsByCategory(
    "Office"
    );
    assertThat(toDTOs(responseJson))
    .extracting(ProductDTO::getId)
    .containsOnly(
    "4243"
    ,
    "1123"
    );
    }
    Если вы стараетесь писать тесты максимально компактно (что я, в любом случае, горячо рекомендую), то переиспользуемые значения хорошо видны. Сам код становится более
    убористым и хорошо читаемым. И, наконец, сообщение об ошибке приведёт вас точно к той строке, где ошибка возникла.
    Не расширяйте существующие тесты, чтобы «добавить ещё одну
    маленькую штучку»
    // Неправильно
    public class
    ProductControllerTest
    {
    @Test public void happyPath
    () {
    // здесь много кода...
    }
    }
    Всегда есть соблазн добавить частный случай к существующему тесту, проверяющему базовую функциональность. Но в результате тесты становятся больше и сложнее для понимания. Частные случаи, раскиданные по большой простыне кода, легко не заметить. Если тест сломался, вы не сразу поймёте, что именно послужило причиной.
    // Правильно
    public class
    ProductControllerTest
    {
    @Test public void multipleProductsAreReturned
    () {}
    @Test public void allProductValuesAreReturned
    () {}
    @Test public void filterByCategory
    () {}

    @Test public void filterByDateCreated
    () {}
    }
    Вместо этого напишите новый тест с наглядным названием, из которого сразу будет понятно, какого поведения он ожидает от тестируемого кода. Да, придётся набрать больше букв на клавиатуре (против этого, напомню, хорошо способствуют вспомогательные функции), но зато вы получите простой и понятный тест с предсказуемым результатом. Это, кстати, отличный способ документировать новый функционал.
    Проверяйте только то, что хотите протестировать
    Думайте о том функционале, который тестируется. Избегайте делать лишние проверки просто потому, что есть такая возможность. Более того, помните о том, что уже проверялось в ранее написанных тестах и не проверяйте это повторно. Тесты должны быть компактными и их ожидаемое поведение должно быть очевидным и лишённым ненужных подробностей.
    Предположим, что мы хотим проверить HTTP-ручку, возвращающую список товаров. Наш тестовый набор должен содержать следующие тесты:
    1. Один большой тест маппинга, который проверяет, что все значения из БД корректно возвращаются в JSON-ответе и правильно присваиваются в нужном формате. Мы легко можем написать это при помощи функций isEqualTo()
    (для единичного элемента) или containsOnly()
    (для множества элементов) из пакета AssertJ, если вы правильно реализуете метод equals()
    String responseJson = requestProducts();
    ProductDTO expectedDTO1 = new
    ProductDTO(
    "1"
    ,
    "envelope"
    , new
    Category(
    "office"
    ),
    List.of(States.ACTIVE, States.REJECTED));
    ProductDTO expectedDTO2 = new
    ProductDTO(
    "2"
    ,
    "envelope"
    , new
    Category(
    "smartphone
    "
    ), List.of(States.ACTIVE));
    assertThat(toDTOs(responseJson))
    .containsOnly(expectedDTO1, expectedDTO2);
    2. Несколько тестов, проверяющих корректное поведение параметра ?category. Здесь мы хотим
    проверить только правильную работу фильтров, а не значения свойств, потому что мы сделали это раньше. Следовательно, нам достаточно проверить совпадения полученных id товаров:
    String responseJson = requestProductsByCategory(
    "Office"
    );
    assertThat(toDTOs(responseJson))
    .extracting(ProductDTO::getId)
    .containsOnly(
    "1"
    ,
    "2"
    );
    3. Ещё пару тестов, проверяющих особые случаи или особую бизнес-логику, например что определённые значения в ответе вычислены корректно. В этом случае нас интересуют только несколько полей из всего JSON-ответа. Тем самым мы своим тестом документируем именно эту специальную логику. Понятно, что ничего кроме этих полей нам здесь не нужно.
    assertThat(actualProduct.getPrice()).isEqualTo(
    100
    );
    Самодостаточные тесты
    Не прячьте релевантные параметры (во вспомогательных
    функциях)
    // Неправильно
    insertIntoDatabase(createProduct());
    List actualProducts = requestProductsByCategory();
    assertThat(actualProducts).containsOnly(
    new
    ProductDTO(
    "1"
    ,
    "Office"
    ));
    Использовать вспомогательные функции для генерации данных и проверки условий удобно, но их следует вызывать с параметрами. Принимайте на вход параметры для всего, что значимо в рамках теста и должно контролироваться из кода теста. Не заставляйте читателя переходить внутрь вспомогательной функции, чтобы понять смысл теста. Простое правило: смысл теста должен быть понятен при взгляде на сам тест.
    // Правильно
    insertIntoDatabase(createProduct(
    "1"
    ,
    "Office"
    ));
    List actualProducts = requestProductsByCategory(
    "Office"
    );
    assertThat(actualProducts).containsOnly(
    new
    ProductDTO(
    "1"
    ,
    "Office"
    ));
    Держите тестовые данные внутри самих тестов
    Всё должно быть внутри. Велик соблазн перенести часть данных в метод
    @Before и переиспользовать их оттуда. Но это вынудит читателя скакать туда-сюда по файлу, чтобы понять, что именно тут происходит. Опять же, вспомогательные функции помогут избежать повторений и сделают тесты более понятными.
    Используйте композицию вместо наследования
    Не выстраивайте сложных иерархий тестовых классов.
    // Неправильно
    class
    SimpleBaseTest
    {}
    class
    AdvancedBaseTest extends
    SimpleBaseTest
    {}
    class
    AllInklusiveBaseTest extends
    AdvancedBaseTest
    {}
    class
    MyTest extends
    AllInklusiveBaseTest
    {}
    Такие иерархии усложняют понимание и вы, скорее всего, быстро обнаружите себя пишущим очередного наследника базового теста, внутри которого зашито множество хлама, который текущему тесту вовсе не нужен. Это отвлекает читателя и приводит к трудноуловимым ошибкам.
    Наследование не гибко: как вы сами думаете, можно ли использовать все методы класса
    AllInclusiveBaseTest
    , но ни одного из его родительского
    AdvancedBaseTest?
    Более того, читателю придётся постоянно прыгать между различными базовыми классами, чтобы понять общую картину.
    «Лучше продублировать код, чем выбрать неправильную абстракцию» (Sandi Metz)
    Вместо этого я рекомендую использовать композицию. Напишите маленькие фрагменты кода и классы для каждой задачи, связанной с фикстурами (запустить тестовую базу данных, создать схему, вставить данные, запустить мок-сервер). Переиспользуйте эти запчасти в методе
    @BeforeAll или через присвоение созданных объектов полям тестового класса. Таким образом, вы сможете собирать каждый новый тестовый класс из этих заготовок, как из деталей

    Лего. В результате каждый тест будет иметь свой собственный понятный набор фикстур и гарантировать, что в нём не происходит ничего постороннего. Тест становится самодостаточным, потому что содержит в себе всё необходимое.
    // Правильно
    public class
    MyTest
    {
    // композиция вместо наследования
    private
    JdbcTemplate template;
    private
    MockWebServer taxService;
    @BeforeAll public void setupDatabaseSchemaAndMockWebServer
    () throws
    IOException {
    this
    .template = new
    DatabaseFixture().startDatabaseAndCreateSchema();
    this
    .taxService = new
    MockWebServer();
    taxService.start();
    }
    }
    // В другом файле
    public class
    DatabaseFixture
    {
    public
    JdbcTemplate startDatabaseAndCreateSchema
    () throws
    IOException {
    PostgreSQLContainer db = new
    PostgreSQLContainer(
    "postgres:11.2-alpine"
    );
    db.start();
    DataSource dataSource = DataSourceBuilder.create()
    .driverClassName(
    "org.postgresql.Driver"
    )
    .username(db.getUsername())
    .password(db.getPassword())
    .url(db.getJdbcUrl())

    .build();
    JdbcTemplate template = new
    JdbcTemplate(dataSource);
    SchemaCreator.createSchema(template);
    return template;
    }
    }
    И ещё раз:
    «KISS важнее DRY»
    Прямолинейные тесты — это хорошо. Сравнивайте результат с константами
    Не переиспользуйте продакшн-код
    Тесты должны проверять продакшн-код, а не переиспользовать его. Если вы переиспользуете боевой код в тесте, вы можете пропустить баг в этом коде, потому что больше не тестируете его.
    // Неправильно
    boolean isActive = true
    ;
    boolean isRejected = true
    ;
    insertIntoDatabase(
    new
    Product(
    1
    , isActive, isRejected));
    ProductDTO actualDTO = requestProduct(
    1
    );
    // переиспользование боевого кода
    List expectedStates = ProductionCode.mapBooleansToEnumList(isActive, isReje cted);
    assertThat(actualDTO.states).isEqualTo(expectedStates);
    Вместо этого при написании тестов думайте в терминах ввода и вывода. Тест подаёт данные на вход и сравнивает вывод с предопределёнными константами. Бóльшую часть времени переиспользование кода не требуется.
    // Do
    assertThat(actualDTO.states).isEqualTo(List.of(States.ACTIVE, States.REJECTED));
    Не копируйте бизнес-логику в тесты
    Маппинг объектов — яркий пример случая, когда тесты тащат в себя логику из боевого кода.
    Предположим наш тест содержит метод mapEntityToDto()
    , результат выполнения которого используется для проверки, что полученный DTO содержит те же значения, что и элементы, которые были добавлены в базу в начале теста. В этом случае вы, скорее всего, скопируете в тест боевой код, который может содержать ошибки.
    // Неправильно
    ProductEntity inputEntity = new
    ProductEntity(
    1
    ,
    "envelope"
    ,
    "office"
    , false
    , true
    ,
    200
    ,
    10.0
    );
    insertIntoDatabase(input);
    ProductDTO actualDTO = requestProduct(
    1
    );
    // mapEntityToDto() содержит ту же логику, что и продакшн-код
    ProductDTO expectedDTO = mapEntityToDto(inputEntity);
    assertThat(actualDTO).isEqualTo(expectedDTO);
    Правильным будет решение, при котором actualDTO
    сравнивается с созданным вручную эталонным объектом с заданными значениями. Это предельно просто, понятно и защищает от потенциальных ошибок.

    // Правильно
    ProductDTO expectedDTO = new
    ProductDTO(
    "1"
    ,
    "envelope"
    , new
    Category(
    "office"
    ), L
    ist.of(States.ACTIVE, States.REJECTED))
    assertThat(actualDTO).isEqualTo(expectedDTO);
    Если вы не хотите создавать и проверять на совпадение целый эталонный объект, можете проверить дочерний объект или вообще только релевантные тесту свойства объекта.
    Не пишите слишком много логики
    Напомню, что тестирование касается в основном ввода и вывода. Подавайте на вход данные и проверяйте, что вам вернулось. Нет необходимости писать сложную логику внутри тестов. Если вы вводите в тест циклы и условия, вы делаете его менее понятным и более неустойчивым к ошибкам. Если ваша логика проверки сложна, пользуйтесь многочисленными функциями AssertJ, которые сделают эту работу за вас.
    Запускайте тесты в среде, максимально похожей на боевую
    Тестируйте максимально полную связку компонентов
    Обычно рекомендуется тестировать каждый класс изолированно при помощи моков. У этого подхода, однако, есть и недостатки: таким образом не тестируется взаимодействие классов между собой, и любой рефакторинг общих сущностей сломает все тесты разом, потому что у каждого внутреннего класса свои тесты. Кроме того, если писать тесты для каждого класса, то их будет просто слишком много.

    Изолированное юнит-тестирование каждого класса
    Вместо этого я рекомендую сосредоточиться на интеграционном тестировании. Под
    «интеграционным тестированием» я подразумеваю сбор всех классов воедино (как на продакшене) и тестирование всей связки, включая инфраструктурные компоненты (HTTP-сервер, базу данных, бизнес-логику). В этом случае вы тестируете поведение вместо реализации. Такие тесты более аккуратны, близки к реальному миру и устойчивы к рефакторингу внутренних компонентов. В идеале, вам будет достаточно одного класса тестов.
    Интеграционное тестирование (= собрать все классы вместе и тестировать связку)

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


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