- Интеграционное тестирование БД с помощью MariaDB для подмены MySql
- Реализация мультиязычности приложения
- Сохранение файлов в приложение и данных о них на БД
Виды тестирования
Что такое тест? Как гласит Вики: «Тест или испытание — способ изучения глубинных процессов деятельности системы посредством помещения системы в разные ситуации и отслеживание доступных наблюдению изменений в ней». Иными словами, это проверка правильности работы нашей системы в тех или иных ситуациях. Что же, посмотрим, какие вообще есть виды тестирования:Модульное тестирование (unit testing) — тесты, задача которых проверить каждый модуль системы по отдельности. Желательно, чтобы это были минимально делимые кусочки системы, например, модули.
Системное тестирование (system testing) — тест высокого уровня для проверки работы большего куска приложения или системы в целом.
Регрессионное тестирование (regression testing) — тестирование, которое используется для проверки того, не влияют ли новые фичи или исправленные баги на существующий функционал приложения и не появляются ли старые баги.
Функциональное тестирование (functional testing) — проверка соответствия части приложения требованиям, заявленным в спецификациях, юзерсторях и т. д.
Виды функционального тестирования:
- тест «белого ящика» (white box) на соответствие части приложения требованиям со знанием внутренней реализации системы;
- тест «черного ящика» (black box) на соответствие части приложения требованиям без знания внутренней реализации системы.
- Тестирование производительности (performance testing) — вид тестов, которые пишутся для определения скорости отработки системы или ее части под определённой нагрузкой.
- Нагрузочное тестирование (load testing) — тесты, предназначенные для проверки устойчивости системы при стандартных нагрузках и для нахождения максимально возможного пика, при котором приложение работает корректно.
- Стресс-тестирование (stress testing) — вид тестирования, предназначенный для проверки работоспособности приложения при нестандартных нагрузках и для определения максимально возможного пика, при котором система не упадёт.
- Тестирование безопасности (security testing) — тесты, используемые для проверки безопасности системы (от атак хакеров, вирусов, несанкционированного доступа к конфиденциальным данным и прочих радостей жизни).
- Тестирование локализации (localization testing) — это тесты локализации для приложения.
- Юзабилити тестирование (usability testing) — вид тестирования, направленный на проверку удобства использования, понятности, привлекательности и обучаемости для пользователей. Это всё звучит хорошо, но как оно происходит на практике? Все просто: используется пирамида тестирования Майка Кона: Это упрощенный вариант пирамиды: сейчас её делят на более мелкие детали. Но сегодня мы не будем извращаться и рассмотрим самый простой вариант.
Unit — модульные тесты, применяемые в различных слоях приложения, тестирующие наименьшую делимую логику приложения: например, класс, но чаще всего — метод. Эти тесты обычно стараются по максимуму изолировать от внешней логики, то есть создать иллюзию того, что остальная часть приложения работает в стандартном режиме.
Данных тестов всегда должно быть много (больше, чем остальных видов), так как они тестируют маленькие кусочки и весьма легковесные, не кушающие много ресурсов (под ресурсами я имею виду оперативную память и время).
Integration — интеграционное тестирование. Оно проверяет более крупные кусочки системы, то есть это либо объединение нескольких кусочков логики (несколько методов или классов), либо корректность работы с внешним компонентом. Этих тестов как правило меньше, чем Unit, так как они тяжеловеснее.
Как пример интеграционных тестов можно рассмотреть соединение с базой данных и проверку правильной отработки методов, работающих с ней.
UI — тесты, которые проверяют работу пользовательского интерфейса. Они затрагивают логику на всех уровнях приложения, из-за чего их еще называют сквозными. Их как правило в разы меньше, так они наиболее тяжеловесны и должны проверять самые необходимые (используемые) пути.
На рисунке выше мы видим соотношение площадей разных частей треугольника: примерно такая же пропорция сохраняется в количестве этих тестов в реальной работе.
Сегодня подробно рассмотрим самые используемые тесты — юнит-тесты, так как уметь ими пользоваться на базовом уровне должны все уважающие себя Java-разработчики.
- материал о Code Coverage на JavaRush и на Хабре;
- фундаментальная теория тестирования.
- Пишем наш тест.
- Запускаем тест, прошел он или нет (видим, что всё красное — не психуем: так и должно быть).
- Добавляем код, который должен удовлетворить данный тест (запускаем тест).
- Выполняем рефакторинг кода.
- Задание тестируемых данных (фикстур).
- Использование тестируемого кода (вызов тестируемого метода).
- Проверка результатов и сверка с ожидаемыми.
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.
Ключевые понятия юнит-тестирования
Покрытие тестов (Code Coverage) — одна из главных оценок качества тестирования приложения. Это процент кода, который был покрыт тестами (0-100%). На практике многие гонятся за этим процентом, с чем я не согласен, так как начинается навешивание тестов там, где они не нужны. Например, у нас в сервисе есть стандартные CRUD (create/get/update/delete) операции без дополнительной логики. Эти методы — сугубо посредники, делегирующие работу слою, работающему с репозиторием. В данной ситуации нам нечего тестировать: разве то, что вызывает ли данный метод — метод из дао, но это не серьёзно. Для оценки покрытия тестами обычно используют дополнительные инструменты: JaCoCo, Cobertura, Clover, Emma и т.д. Для более детального изучения данного вопроса держи пару годных статей:Этапы тестирования
Тест состоит из трёх этапов:Среды тестирования
Итак, теперь ближе к делу. Для Java доступно несколько сред тестирования (фреймворков). Самые популярные из них — JUnit и TestNG. Для нашего обзора мы используем: JUnit тест представляет собой метод, содержащийся в классе, который используется только для тестирования. Класс, как правило, называется так же, как и класс, который он тестирует с +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. Взглянем на некоторые методы сравнения результатов: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 – так как в экземпляре для обновления мы не задавали это поле, оно должно остаться прежним, проверяем это.
Запускаем:
Тест зелёный, можно выдыхать))
Итак, подведём итоги: тестирование улучшает качество кода и делает процесс разработки более гибким и надёжный.
Представьте себе, как много сил мы потратим при изменении дизайна программного обеспечения с сотнями файлов классов. Когда у нас есть модульные тесты, написанные для всех этих классов, мы можем уверенно провести рефакторинг. И самое главное — это помогает нам легко находить ошибки во время разработки.
Гайз, на этом у меня сегодня всё: сыпем лайки, пишем комменты)))
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ