JavaRush /Java Blog /Random-TW /Java 單元測試:技術、概念、實踐

Java 單元測試:技術、概念、實踐

在 Random-TW 群組發布
今天,您幾乎找不到沒有測試的應用程序,因此這個主題對於新手開發人員來說比以往任何時候都更加相關:沒有測試,您將一事無成。作為廣告,我建議你看看我以前的文章。其中一些涵蓋測試(即使如此,這些文章也將非常有用):
  1. 使用MariaDB取代MySql的資料庫整合測試
  2. 多語言應用的實施
  3. 將文件保存到應用程式並將有關它們的資料保存到資料庫
讓我們考慮一下原則上使用哪些類型的測試,然後我們將詳細研究您需要了解的有關單元測試的所有內容。

測試類型

什麼是測試?正如維基百科所說:“測試測試是通過將系統置於不同的情況下並跟踪其中可觀察到的變化來研究系統底層過程的一種方法。” 換句話說,這是對我們系統在某些情況下能否正確運作的測試。關於單元測試:方法、概念、實踐 - 2好吧,讓我們看看有哪些類型的測試:
  1. 單元測試是其任務是單獨測試系統的每個模組的測試。期望這些是系統的最小可分割部分,例如模組。

  2. 系統測試是一種進階測試,用於測試應用程式的較大部分或整個系統的運作。

  3. 回歸測試是用於檢查新功能或錯誤修復是否會影響應用程式現有功能以及舊錯誤是否再次出現的測試。

  4. 功能測試是檢查應用程式的一部分是否符合規範、使用者故事等中規定的要求。

    功能測試的類型:

    • 「白盒」測試,在了解系統內部實現的情況下,測試部分應用程式是否符合要求;
    • 「黑盒」測試用於在不了解系統內部實現的情況下部分應用程式是否符合要求。
  5. 效能測試是一種測試,旨在確定係統或其一部分在特定負載下運行的速度。
  6. 負載測試- 旨在檢查系統在標準負載下的穩定性並找到應用程式正常工作的最大可能峰值的測試。
  7. 壓力測試是一種測試,旨在檢查非標準負載下應用程式的功能,並確定係統不會崩潰的最大可能峰值。
  8. Тестирование безопасности (security testing) — тесты, используемые для проверки безопасности системы (от атак хакеров, вирусов, несанкционированного доступа к конфиденциальным данным и прочих радостей жизни).
  9. Тестирование локализации (localization testing) — это тесты локализации для applications.
  10. Юзабorти тестирование (usability testing) — вид тестирования, направленный на проверку удобства использования, понятности, привлекательности и обучаемости для пользователей.
  11. Это всё звучит хорошо, но How оно происходит на практике? Все просто: используется пирамида тестирования Майка Кона: 關於單元測試:方法、概念、實踐 - 4Это упрощенный вариант пирамиды: сейчас её делят на более мелкие детали. Но сегодня мы не будем извращаться и рассмотрим самый простой вариант.
    1. Unit — модульные тесты, применяемые в различных слоях applications, тестирующие наименьшую делимую логику applications: например, класс, но чаще всего — метод. Эти тесты обычно стараются по максимуму изолировать от внешней логики, то есть создать иллюзию того, что остальная часть applications работает в стандартном режиме.

      Данных тестов всегда должно быть много (больше, чем остальных видов), так 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 более понятным, простым и без ошибок. 關於單元測試:方法、概念、實踐 - 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. Проверка результатов и сверка с ожидаемыми.
    關於單元測試:方法、概念、實踐 - 7Whatбы обеспечить модульность теста, нужно нужно изолироваться от других слоев applications. Сделать это можно помощью заглушек, моков и шпионов. Мок (Mock) — an objectы, которые настраиваются (например, специфично для каждого теста) и позволяют задать ожидания вызовы методов в виде ответов, которые мы планируем получить. Проверки соответствия ожиданиям проводятся через вызовы к Mock-an objectм. Заглушки (Stub) — обеспечивают жестко зашитый ответ на вызовы во время тестирования. Также они могут сохранять в себе информацию о вызове (например, параметры or количество этих вызовов). Такие иногда называют своим термином — шпион (Spy). Иногда эти термины stubs и mock путают: разница в том, что стаб ничего не проверяет, а лишь имитирует заданное состояние. А мок — это an object, у которого есть ожидания. Например, что данный метод класса должен быть вызван определенное число раз. Иными словами, ваш тест никогда не сломается из-за «стаба», а вот из-за мока может.

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

    Итак, теперь ближе к делу. Для Java доступно несколько сред тестирования (фреймворков). Самые популярные из них — JUnit и TestNG. Для нашего обзора мы используем: 關於單元測試:方法、概念、實踐 - 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 в экземпляре для обновления мы не задавали это поле, оно должно остаться прежним, проверяем это. 關於單元測試:方法、概念、實踐 - 9Запускаем: 關於單元測試:技術、概念、實務 - 10Тест зелёный, можно выдыхать)) Итак, подведём итоги: тестирование улучшает качество codeа и делает процесс разработки более гибким и надёжный. Представьте себе, How много сил мы потратим при изменении дизайна программного обеспечения с сотнями файлов классов. Когда у нас есть модульные тесты, написанные для всех этих классов, мы можем уверенно провести рефакторинг. И самое главное — это помогает нам легко находить ошибки во время разработки. Гайз, на этом у меня сегодня всё: сыпем лайки, пишем комменты))) 關於單元測試:方法、概念、實踐 - 11
留言
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION