測試類型
什麼是測試?正如維基百科所說:“測試或測試是通過將系統置於不同的情況下並跟踪其中可觀察到的變化來研究系統底層過程的一種方法。” 換句話說,這是對我們系統在某些情況下能否正確運作的測試。好吧,讓我們看看有哪些類型的測試:-
單元測試是其任務是單獨測試系統的每個模組的測試。期望這些是系統的最小可分割部分,例如模組。
-
系統測試是一種進階測試,用於測試應用程式的較大部分或整個系統的運作。
-
回歸測試是用於檢查新功能或錯誤修復是否會影響應用程式現有功能以及舊錯誤是否再次出現的測試。
-
功能測試是檢查應用程式的一部分是否符合規範、使用者故事等中規定的要求。
功能測試的類型:
- 「白盒」測試,在了解系統內部實現的情況下,測試部分應用程式是否符合要求;
- 「黑盒」測試用於在不了解系統內部實現的情況下部分應用程式是否符合要求。
- 效能測試是一種測試,旨在確定係統或其一部分在特定負載下運行的速度。
- 負載測試- 旨在檢查系統在標準負載下的穩定性並找到應用程式正常工作的最大可能峰值的測試。
- 壓力測試是一種測試,旨在檢查非標準負載下應用程式的功能,並確定係統不會崩潰的最大可能峰值。
- Тестирование безопасности (security testing) — тесты, используемые для проверки безопасности системы (от атак хакеров, вирусов, несанкционированного доступа к конфиденциальным данным и прочих радостей жизни).
- Тестирование локализации (localization testing) — это тесты локализации для applications.
- Юзабorти тестирование (usability testing) — вид тестирования, направленный на проверку удобства использования, понятности, привлекательности и обучаемости для пользователей. Это всё звучит хорошо, но How оно происходит на практике? Все просто: используется пирамида тестирования Майка Кона: Это упрощенный вариант пирамиды: сейчас её делят на более мелкие детали. Но сегодня мы не будем извращаться и рассмотрим самый простой вариант.
-
Unit — модульные тесты, применяемые в различных слоях applications, тестирующие наименьшую делимую логику applications: например, класс, но чаще всего — метод. Эти тесты обычно стараются по максимуму изолировать от внешней логики, то есть создать иллюзию того, что остальная часть applications работает в стандартном режиме.
Данных тестов всегда должно быть много (больше, чем остальных видов), так How они тестируют маленькие кусочки и весьма легковесные, не кушающие много ресурсов (под ресурсами я имею виду оперативную память и время).
-
Integration — интеграционное тестирование. Оно проверяет более крупные кусочки системы, то есть это либо объединение нескольких кусочков логики (несколько методов or классов), либо корректность работы с внешним компонентом. Этих тестов How правило меньше, чем Unit, так How они тяжеловеснее.
Как пример интеграционных тестов можно рассмотреть соединение с базой данных и проверку правильной отработки методов, работающих с ней.
-
UI — тесты, которые проверяют работу пользовательского интерфейса. Они затрагивают логику на всех уровнях applications, из-за чего их еще называют сквозными. Их How правило в разы меньше, так они наиболее тяжеловесны и должны проверять самые необходимые (используемые) пути.
На рисунке выше мы видим соотношение площадей разных частей треугольника: примерно такая же пропорция сохраняется в количестве этих тестов в реальной работе.
Сегодня подробно рассмотрим самые используемые тесты — юнит-тесты, так How уметь ими пользоваться на базовом уровне должны все уважающие себя Java-разработчики.
- материал о Code Coverage на JavaRush и на Хабре;
- фундаментальная теория тестирования.
- Пишем наш тест.
- Запускаем тест, прошел он or нет (видим, что всё красное — не психуем: так и должно быть).
- Добавляем code, который должен удовлетворить данный тест (запускаем тест).
- Выполняем рефакторинг codeа.
- Задание тестируемых данных (фикстур).
- Использование тестируемого codeа (вызов тестируемого метода).
- Проверка результатов и сверка с ожидаемыми.
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.
Ключевые понятия юнит-тестирования
Покрытие тестов (Code Coverage) — одна из главных оценок качества тестирования applications. Это процент codeа, который был покрыт тестами (0-100%). На практике многие гонятся за этим процентом, с чем я не согласен, так How начинается навешивание тестов там, где они не нужны. Например, у нас в сервисе есть стандартные CRUD (create/get/update/delete) операции без дополнительной логики. Эти методы — сугубо посредники, делегирующие работу слою, работающему с репозиторием. В данной ситуации нам нечего тестировать: разве то, что вызывает ли данный метод — метод из дао, но это не серьёзно. Для оценки покрытия тестами обычно используют дополнительные инструменты: JaCoCo, Cobertura, Clover, Emma и т.д. Для более детального изучения данного вопроса держи пару годных статей:Этапы тестирования
Тест состоит из трёх этапов:Среды тестирования
Итак, теперь ближе к делу. Для Java доступно несколько сред тестирования (фреймворков). Самые популярные из них — JUnit и TestNG. Для нашего обзора мы используем: JUnit тест представляет собой метод, содержащийся в классе, который используется только для тестирования. Класс, 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. Взглянем на некоторые методы сравнения результатов: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 в экземпляре для обновления мы не задавали это поле, оно должно остаться прежним, проверяем это. Запускаем: Тест зелёный, можно выдыхать)) Итак, подведём итоги: тестирование улучшает качество codeа и делает процесс разработки более гибким и надёжный. Представьте себе, How много сил мы потратим при изменении дизайна программного обеспечения с сотнями файлов классов. Когда у нас есть модульные тесты, написанные для всех этих классов, мы можем уверенно провести рефакторинг. И самое главное — это помогает нам легко находить ошибки во время разработки. Гайз, на этом у меня сегодня всё: сыпем лайки, пишем комменты)))
GO TO FULL VERSION