JavaRush/Java блог/Random/Java Unit Testing: методики, понятия, практика
Константин
36 уровень

Java Unit Testing: методики, понятия, практика

Статья из группы Random
участников
Сегодня и не встретишь приложения, не обвешанного тестами, поэтому эта тема будет как никогда актуальна для начинающих разработчиков: без тестов — никуда. На правах рекламы предложу просмотреть мои прошлые статьи. В некоторых из них затрагиваются тесты (да и так статьи будут весьма полезны):
  1. Интеграционное тестирование БД с помощью MariaDB для подмены MySql
  2. Реализация мультиязычности приложения
  3. Сохранение файлов в приложение и данных о них на БД
Рассмотрим, какие виды тестирования используют в принципе, а после этого детально изучим все, что нужно знать о юнит-тестировании.

Виды тестирования

Что такое тест? Как гласит Вики: «Тест или испытание — способ изучения глубинных процессов деятельности системы посредством помещения системы в разные ситуации и отслеживание доступных наблюдению изменений в ней». Иными словами, это проверка правильности работы нашей системы в тех или иных ситуациях. Все о Unit testing: методики, понятия, практика - 2Что же, посмотрим, какие вообще есть виды тестирования:
  1. Модульное тестирование (unit testing) — тесты, задача которых проверить каждый модуль системы по отдельности. Желательно, чтобы это были минимально делимые кусочки системы, например, модули.

  2. Системное тестирование (system testing) — тест высокого уровня для проверки работы большего куска приложения или системы в целом.

  3. Регрессионное тестирование (regression testing) — тестирование, которое используется для проверки того, не влияют ли новые фичи или исправленные баги на существующий функционал приложения и не появляются ли старые баги.

  4. Функциональное тестирование (functional testing) — проверка соответствия части приложения требованиям, заявленным в спецификациях, юзерсторях и т. д.

    Виды функционального тестирования:

    • тест «белого ящика» (white box) на соответствие части приложения требованиям со знанием внутренней реализации системы;
    • тест «черного ящика» (black box) на соответствие части приложения требованиям без знания внутренней реализации системы.
  5. Тестирование производительности (performance testing) — вид тестов, которые пишутся для определения скорости отработки системы или ее части под определённой нагрузкой.
  6. Нагрузочное тестирование (load testing) — тесты, предназначенные для проверки устойчивости системы при стандартных нагрузках и для нахождения максимально возможного пика, при котором приложение работает корректно.
  7. Стресс-тестирование (stress testing) — вид тестирования, предназначенный для проверки работоспособности приложения при нестандартных нагрузках и для определения максимально возможного пика, при котором система не упадёт.
  8. Тестирование безопасности (security testing) — тесты, используемые для проверки безопасности системы (от атак хакеров, вирусов, несанкционированного доступа к конфиденциальным данным и прочих радостей жизни).
  9. Тестирование локализации (localization testing) — это тесты локализации для приложения.
  10. Юзабилити тестирование (usability testing) — вид тестирования, направленный на проверку удобства использования, понятности, привлекательности и обучаемости для пользователей.
  11. Это всё звучит хорошо, но как оно происходит на практике? Все просто: используется пирамида тестирования Майка Кона: Все о Unit testing: методики, понятия, практика - 4Это упрощенный вариант пирамиды: сейчас её делят на более мелкие детали. Но сегодня мы не будем извращаться и рассмотрим самый простой вариант.
    1. Unit — модульные тесты, применяемые в различных слоях приложения, тестирующие наименьшую делимую логику приложения: например, класс, но чаще всего — метод. Эти тесты обычно стараются по максимуму изолировать от внешней логики, то есть создать иллюзию того, что остальная часть приложения работает в стандартном режиме.

      Данных тестов всегда должно быть много (больше, чем остальных видов), так как они тестируют маленькие кусочки и весьма легковесные, не кушающие много ресурсов (под ресурсами я имею виду оперативную память и время).

    2. Integration — интеграционное тестирование. Оно проверяет более крупные кусочки системы, то есть это либо объединение нескольких кусочков логики (несколько методов или классов), либо корректность работы с внешним компонентом. Этих тестов как правило меньше, чем Unit, так как они тяжеловеснее.

      Как пример интеграционных тестов можно рассмотреть соединение с базой данных и проверку правильной отработки методов, работающих с ней.

    3. UI — тесты, которые проверяют работу пользовательского интерфейса. Они затрагивают логику на всех уровнях приложения, из-за чего их еще называют сквозными. Их как правило в разы меньше, так они наиболее тяжеловесны и должны проверять самые необходимые (используемые) пути.

      На рисунке выше мы видим соотношение площадей разных частей треугольника: примерно такая же пропорция сохраняется в количестве этих тестов в реальной работе.

      Сегодня подробно рассмотрим самые используемые тесты — юнит-тесты, так как уметь ими пользоваться на базовом уровне должны все уважающие себя Java-разработчики.

    Ключевые понятия юнит-тестирования

    Покрытие тестов (Code Coverage) — одна из главных оценок качества тестирования приложения. Это процент кода, который был покрыт тестами (0-100%). На практике многие гонятся за этим процентом, с чем я не согласен, так как начинается навешивание тестов там, где они не нужны. Например, у нас в сервисе есть стандартные CRUD (create/get/update/delete) операции без дополнительной логики. Эти методы — сугубо посредники, делегирующие работу слою, работающему с репозиторием. В данной ситуации нам нечего тестировать: разве то, что вызывает ли данный метод — метод из дао, но это не серьёзно. Для оценки покрытия тестами обычно используют дополнительные инструменты: JaCoCo, Cobertura, Clover, Emma и т.д. Для более детального изучения данного вопроса держи пару годных статей: TDD (Test-driven development) — разработка через тестирование. В рамках этого подхода в первую очередь пишется тест, который будет проверять определенный код. Получается тестирование чёрного ящика: мы знаем, что есть на входе и знаем, что должно получиться на выходе. Это позволяет избежать дублирования кода. Разработка через тестирование начинается с проектирования и разработки тестов для каждой небольшой функциональности приложения. В подходе TDD, во-первых, разрабатывается тест, который определяет и проверяет, что будет делать код. Основная цель TDD — сделать код более понятным, простым и без ошибок. Все о Unit testing: методики, понятия, практика - 6Подход состоит из таких составляющих:
    1. Пишем наш тест.
    2. Запускаем тест, прошел он или нет (видим, что всё красное — не психуем: так и должно быть).
    3. Добавляем код, который должен удовлетворить данный тест (запускаем тест).
    4. Выполняем рефакторинг кода.
    Исходя из того, что модульные тесты являются наименьшими элементами в пирамиде автоматизации тестирования, TDD основан на них. С помощью модульных тестов мы можем проверить бизнес-логику любого класса. BDD (Behavior-driven development) — разработка через поведение. Это подход основан на TDD. Если говорить точнее, он использует написанные понятным языком примеры (как правило на английском), которые иллюстрируют поведение системы для всех, кто участвует в разработке. Не будем углубляться в данный термин, так как он в основном затрагивает тестировщиков и бизнес-аналитиков. Тестовый сценарий (Test Case) — сценарий, описывающий шаги, конкретные условия и параметры, необходимые для проверки реализации тестируемого кода. Фикстуры (Fixture) — состояние среды тестирования, которое необходимо для успешного выполнения испытуемого метода. Это заранее заданный набор объектов и их поведения в используемых условиях.

    Этапы тестирования

    Тест состоит из трёх этапов:
    1. Задание тестируемых данных (фикстур).
    2. Использование тестируемого кода (вызов тестируемого метода).
    3. Проверка результатов и сверка с ожидаемыми.
    Все о Unit testing: методики, понятия, практика - 7Чтобы обеспечить модульность теста, нужно нужно изолироваться от других слоев приложения. Сделать это можно помощью заглушек, моков и шпионов. Мок (Mock) — объекты, которые настраиваются (например, специфично для каждого теста) и позволяют задать ожидания вызовы методов в виде ответов, которые мы планируем получить. Проверки соответствия ожиданиям проводятся через вызовы к Mock-объектам. Заглушки (Stub) — обеспечивают жестко зашитый ответ на вызовы во время тестирования. Также они могут сохранять в себе информацию о вызове (например, параметры или количество этих вызовов). Такие иногда называют своим термином — шпион (Spy). Иногда эти термины stubs и mock путают: разница в том, что стаб ничего не проверяет, а лишь имитирует заданное состояние. А мок — это объект, у которого есть ожидания. Например, что данный метод класса должен быть вызван определенное число раз. Иными словами, ваш тест никогда не сломается из-за «стаба», а вот из-за мока может.

    Среды тестирования

    Итак, теперь ближе к делу. Для Java доступно несколько сред тестирования (фреймворков). Самые популярные из них — JUnit и TestNG. Для нашего обзора мы используем: Все о Unit testing: методики, понятия, практика - 8JUnit тест представляет собой метод, содержащийся в классе, который используется только для тестирования. Класс, как правило, называется так же, как и класс, который он тестирует с +Test в конце. Например, CarService→ CarServiceTest. Система сборки Maven автоматически включает такие классы в тестовую область. По сути этот класс и называется тестовым. Немного пройдёмся по базовым аннотациям: @Test — определение данного метода в качестве тестируемого (по сути — метод, помеченный данной аннотацией и есть модульный тест). @Before — помечается метод, который будет выполняться перед каждым тестом. Например, заполнение тестовых данных класса, чтение входных данных и т. д. @After — ставится над методом, который будет вызывать после каждого теста (чистка данных, восстановление дефолтных значений). @BeforeClass — ставится над методом — аналог @Before. Но этот метод вызывается лишь однажды перед всеми тестами для данного класса и поэтому должен быть статическим. Он используется для выполнения более тяжелых операций, как например подъем тестовой БД. @AfterClass — противоположность @BeforeClass: исполняется один раз для данного класса, но исполняется после всех тестов. Используется, например, для очистки постоянных ресурсов или отключения от БД. @Ignore — отмечает, что метод ниже отключен и будет игнорироваться при общей прогонке тестов. Используется в разных случаях, например, если изменили базовый метод и не успели переделать под него тест. В таких случаях ещё желательно добавить описание — @Ignore("Some description"). @Test (expected = Exception.class) — используется для отрицательных тестов. Это тесты, которые проверяют, как ведёт себя метод в случае ошибки, то есть тест ожидает, что метод выкинет некоторое исключение. Такой метод обозначается аннотацией @Test, но с указанием ошибки для отлова. @Test(timeout=100) — проверяет, что метод исполняется не более чем 100 миллисекунд. @Mock — используется над полем класс для задания данного объекта моком (это не из Junit библиотеки, а из Mockito), и если нам будет необходимо, мы зададим поведение мока в конкретной ситуации, непосредственно в методе теста. @RunWith(MockitoJUnitRunner.class) — метод ставится над классом. Он и является кнопкой для прогона тестов в нем. Runner-ы могут быть различными: например, есть такие: MockitoJUnitRunner, JUnitPlatform, SpringRunner и т. д.). В JUnit 5 аннотацию @RunWith заменили более мощной аннотацией @ExtendWith. Взглянем на некоторые методы сравнения результатов:
    • assertEquals(Object expecteds, Object actuals) — проверяет, равны ли передаваемые обьекты.
    • assertTrue(boolean flag) — проверяет, возвращает ли переданное значение — true.
    • assertFalse(boolean flag) — проверяет, возвращает ли переданное значение — false.
    • assertNull(Object object) – проверяет, является ли объект нулевым (null).
    • assertSame(Object firstObject, Object secondObject) — проверяет, ссылаются ли передаваемые значения на один и тот же обьект.
    • assertThat(T t, Matcher<T> matcher) — проверяет, удовлетворяет ли t условию, указанному в matcher.
    Ещё есть полезная форма сравнения из assertj — assertThat(firstObject).isEqualTo(secondObject) Здесь я рассказал о базовых методах, так как остальные — это различные вариации приведенных выше.

    Практика тестирования

    А теперь давайте рассмотрим приведенный выше материал на конкретном примере. Будем тестировать метод для сервиса — update. Рассматривать слой дао не будем, так как он у нас дефолтный. Добавим стартер для тестов:
    <dependency>
       <groupId>org.springframework.boot</groupId>
       <artifactId>spring-boot-starter-test</artifactId>
       <version>2.2.2.RELEASE</version>
       <scope>test</scope>
    </dependency>
    Итак, класс сервиса:
    @Service
    @RequiredArgsConstructor
    public class RobotServiceImpl implements RobotService {
       private final RobotDAO robotDAO;
    
       @Override
       public Robot update(Long id, Robot robot) {
           Robot found = robotDAO.findById(id);
           return robotDAO.update(Robot.builder()
                   .id(id)
                   .name(robot.getName() != null ? robot.getName() : found.getName())
                   .cpu(robot.getCpu() != null ? robot.getCpu() : found.getCpu())
                   .producer(robot.getProducer() != null ? robot.getProducer() : found.getProducer())
                   .build());
       }
    }
    8 — вытягиваем обновляемый обьект из БД 9-14 — создаём объект через билдер, если в приходящем объекте есть поле — задаем его, если нет — оставляем то, что есть в БД И смотрим наш тест:
    @RunWith(MockitoJUnitRunner.class)
    public class RobotServiceImplTest {
       @Mock
       private RobotDAO robotDAO;
    
       private RobotServiceImpl robotService;
    
       private static Robot testRobot;
    
       @BeforeClass
       public static void prepareTestData() {
           testRobot = Robot
                   .builder()
                   .id(123L)
                   .name("testRobotMolly")
                   .cpu("Intel Core i7-9700K")
                   .producer("China")
                   .build();
       }
    
       @Before
       public void init() {
           robotService = new RobotServiceImpl(robotDAO);
       }
    1 — наш Runner 4 — изолируем сервис от слоя дао, подставляя мок 11 — задаем для класса тестовую сущность (ту, которую мы будем юзать в качестве испытуемого хомячка) 22 — задаём объект сервиса, который мы и будем тестить
    @Test
    public void updateTest() {
       when(robotDAO.findById(any(Long.class))).thenReturn(testRobot);
       when(robotDAO.update(any(Robot.class))).then(returnsFirstArg());
       Robot robotForUpdate = Robot
               .builder()
               .name("Vally")
               .cpu("AMD Ryzen 7 2700X")
               .build();
    
       Robot resultRobot = robotService.update(123L, robotForUpdate);
    
       assertNotNull(resultRobot);
       assertSame(resultRobot.getId(),testRobot.getId());
       assertThat(resultRobot.getName()).isEqualTo(robotForUpdate.getName());
       assertTrue(resultRobot.getCpu().equals(robotForUpdate.getCpu()));
       assertEquals(resultRobot.getProducer(),testRobot.getProducer());
    }
    Здесь мы видим четкое разделение теста на три части: 3-9 — задание фикстур 11 — выполнение тестируемой части 13-17 — проверка результатов Подробнее: 3-4 — задаём поведение для мока дао 5 — задаём экземпляр, который мы будем апдейтить поверх нашего стандартного 11 — используем метод и берём результирующий экземпляр 13 — проверяем, что он не ноль 14 — сверяем айди результата и заданные аргументы метода 15 — проверяем, обновилось ли имя 16 — смотрим результат по cpu 17 – так как в экземпляре для обновления мы не задавали это поле, оно должно остаться прежним, проверяем это. Все о Unit testing: методики, понятия, практика - 9Запускаем: Все о Unit testing: методики, понятия, практика - 10Тест зелёный, можно выдыхать)) Итак, подведём итоги: тестирование улучшает качество кода и делает процесс разработки более гибким и надёжный. Представьте себе, как много сил мы потратим при изменении дизайна программного обеспечения с сотнями файлов классов. Когда у нас есть модульные тесты, написанные для всех этих классов, мы можем уверенно провести рефакторинг. И самое главное — это помогает нам легко находить ошибки во время разработки. Гайз, на этом у меня сегодня всё: сыпем лайки, пишем комменты))) Все о Unit testing: методики, понятия, практика - 11
Комментарии (14)
  • популярные
  • новые
  • старые
Для того, чтобы оставить комментарий Вы должны авторизоваться
Lika Nauan
Уровень 36
14 декабря 2023, 14:31
в "Ключевые понятия юнит-тестирования" опечатка CRUD (Create, Read, Update, Delete) Возникли вопросы с тем, что такое ДАО, если правильно поняла, вот немного копипасты: Data Access Object (DAO)- структурный шаблон, который позволяет нам изолировать прикладной/бизнес-уровень от постоянного уровня (обычно это реляционная база данных, но это может быть любой другой постоянный механизм) с использованием абстрактного API. DAO позволяет запускать JUnit-тесты быстрее, поскольку позволяет создавать макет и избегать подключения к базе данных для запуска тестов. Это улучшает тестирование, потому что легко написать тест с макетными объектами, а не интеграционный тест с базой данных. В случае какой-либо проблемы при запуске модульного тестирования вам нужно проверять только код, а не базу данных. Также защищает от проблем с подключением к базе данных и средой. Поскольку шаблон DAO основан на интерфейсе, он также продвигает принцип объектно-ориентированного проектирования "программирование для интерфейса, а не для реализации", что приводит к получению гибкого и качественного кода.
Макс Дудин
Уровень 41
20 июня 2022, 14:17
хорошо, но мало...
Евгений
Уровень 1
16 мая 2022, 10:59
них непонятно, но очень интересно
Batman
Уровень 35
5 июля 2021, 18:11
Отличная статья! Спасибо большое автору!! В "@RunWith(MockitoJUnitRunner.class) — метод ставится над классом." - не метод, а аннотация ставится над классом.
Mikhail Morozov QA Automation Engineer
28 февраля 2020, 12:20
свалено в кучу и виды и уровни тестирования. И потом я не совсем уверен что тестовый сценарий это именно test case. Если так ответить на собеседовании - пошлют далеко и надолго …
Andrei Lead Software Developer (
9 февраля 2020, 10:44
@Test(expected = Exception.class) - в JUnit5 добавлен метод assertThrows(), который гораздо эффективней, позволяет проверять не только тип эксепшена, но и дополнительную информацию ошибки, такую как сообщение ошибки и т.п.
Илья Ненашев
Уровень 14
1 февраля 2020, 11:34
интересно
Artem Okunkov
Уровень 22
30 января 2020, 20:56
в общем выглядит все очень даже. А с какого момента курса можно будет что-нибудь руками попробовать пощупать? Ну чтоб с примерами и обучалкой? Или этого нет в рамках основного курса?
Oleksandr
Уровень 41
31 января 2020, 13:01
Java Collections 3 lvl
Artem Okunkov
Уровень 22
2 февраля 2020, 21:17
tnx
28 января 2020, 07:02
Очень неплохо для нулевых знаний начальных. Спасибо!
Nata_anch
Уровень 18
27 января 2020, 16:42
Хорошо структурировано, доступно. Спасибо автору!