JavaRush/Java Blog/Random EN/Java Unit Testing: techniques, concepts, practice

Java Unit Testing: techniques, concepts, practice

Published in the Random EN group
members
Today you will hardly find an application that is not covered with tests, so this topic will be more relevant than ever for novice developers: without tests you can’t get anywhere. As an advertisement, I suggest you look at my past articles. Some of them cover tests (and even so the articles will be very useful):
  1. Integration testing of a database using MariaDB to replace MySql
  2. Implementation of multilingual application
  3. Saving files to the application and data about them to the database
Let's consider what types of testing are used in principle, and after that we will study in detail everything you need to know about unit testing.

Types of testing

What is a test? As Wiki says: “ A test or trial is a way of studying the underlying processes of a system by placing the system in different situations and tracking observable changes in it.” In other words, this is a test of the correct operation of our system in certain situations. All about Unit testing: methods, concepts, practice - 2Well, let's see what types of testing there are:
  1. Unit testing is tests whose task is to test each module of the system individually. It is desirable that these be minimally divisible pieces of the system, for example, modules.

  2. System testing is a high-level test to test the operation of a larger piece of an application or the system as a whole.

  3. Regression testing is testing that is used to check whether new features or bug fixes affect the existing functionality of the application and whether old bugs reappear.

  4. Functional testing is checking the compliance of part of the application with the requirements stated in specifications, user stories, etc.

    Types of functional testing:

    • “white box” test for compliance of part of the application with the requirements with knowledge of the internal implementation of the system;
    • “black box” test for compliance of part of the application with the requirements without knowledge of the internal implementation of the system.
  5. Performance testing is a type of tests that are written to determine the speed at which a system or part of it runs under a certain load.
  6. Load testing - tests designed to check the stability of the system under standard loads and to find the maximum possible peak at which the application works correctly.
  7. Stress testing is a type of testing designed to check the functionality of an application under non-standard loads and to determine the maximum possible peak at which the system will not crash.
  8. Security testing - tests used to check the security of a system (from attacks by hackers, viruses, unauthorized access to confidential data and other pleasures of life).
  9. Localization testing is localization testing for an application.
  10. Usability testing is a type of testing aimed at checking usability, understandability, attractiveness and learnability for users.
  11. This all sounds good, but how does it work in practice? It's simple: Mike Cohn's testing pyramid is used: All about Unit testing: methods, concepts, practice - 4This is a simplified version of the pyramid: now it is divided into smaller parts. But today we will not pervert and consider the simplest option.
    1. Unit - unit tests used in various layers of the application, testing the smallest divisible logic of the application: for example, a class, but most often a method. These tests usually try to isolate as much as possible from external logic, that is, to create the illusion that the rest of the application is working in standard mode.

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

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

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

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

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

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

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

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

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

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

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

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

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

    А теперь давайте рассмотрим приведенный выше материал на конкретном примере. Будем тестировать метод для сервиса — update. Рассматривать слой дао не будем, так How он у нас дефолтный. Добавим стартер для тестов:
    <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 — создаём an object через билдер, если в приходящем an objectе есть поле — задаем его, если нет — оставляем то, что есть в БД И смотрим наш тест:
    @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 — задаём an object сервиса, который мы и будем тестить
    @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 — проверяем, обновилось ли Name 16 — смотрим результат по cpu 17 – так How в экземпляре для обновления мы не задавали это поле, оно должно остаться прежним, проверяем это. All about Unit testing: methods, concepts, practice - 9Запускаем: All about Unit testing: techniques, concepts, practice - 10Тест зелёный, можно выдыхать)) Итак, подведём итоги: тестирование улучшает качество codeа и делает процесс разработки более гибким и надёжный. Представьте себе, How много сил мы потратим при изменении дизайна программного обеспечения с сотнями файлов классов. Когда у нас есть модульные тесты, написанные для всех этих классов, мы можем уверенно провести рефакторинг. И самое главное — это помогает нам легко находить ошибки во время разработки. Гайз, на этом у меня сегодня всё: сыпем лайки, пишем комменты))) All about Unit testing: methods, concepts, practice - 11
Comments
  • Popular
  • New
  • Old
You must be signed in to leave a comment
This page doesn't have any comments yet