Изученной информации уже достаточно для тестирования в повседневной практике разработки. Перед тем, как погружаться в более сложные темы и возможности библиотек для тестирования и фреймворков, пройдём полный путь тестирования библиотеки, поговорим об организации тестов, хороших и плохих практиках. Это поможет сформировать правильное отношение к тестированию в целом.
В этом уроке мы разберём основы модульного тестирования. Это тестирование направлено на проверку модулей программы в изоляции от всех остальных частей. Эти тесты обычно тестируют базовые конструкции языка: методы, пакеты, классы. Такие тесты не дают никаких гарантий работы всего приложения в целом, но хорошо помогают тогда, когда какой-то модуль программы имеет сложную логику.
Попробуем протестировать стек. Стек представляет собой список элементов организованных по принципу LIFO. Данные кладутся в стек в одном порядке, а извлекаются в обратном. Сам стек, как правило, используется для реализации алгоритмов. Он часто используется в низкоуровневом коде: например, внутри языков программирования или в операционных системах. В реальной жизни по принципу стека работает магазин в пистолете: последняя добавленная пуля вылетает первой.
// Стек встроен прямо в Java
import java.util.Stack;
public static void main(String[] args) {
// Если вы не знакомы с дженериками, то не переживайте
// Такой способ создания не влияет на использование
// Он лишь нужен для указания типа данных, которые могут храниться в стеке
Stack<Integer> stack = new Stack<>();
// Проверка на пустоту
stack.isEmpty(); // true;
// Добавление в стек
stack.push(1); // (1)
stack.push(2); // (2, 1)
stack.push(3); // (3, 2, 1)
stack.isEmpty(); // false
// Извлечение из стека
var value = stack.pop(); // 3. В стеке (2, 1)
stack.pop(); // 2. В стеке (1)
stack.pop(); // 1. В стеке пусто
stack.isEmpty(); // true
}
Тестируем основную функциональность
Теперь напишем первый тест. Первый тест всегда должен проверять позитивный сценарий — тот, в котором задействована основная функциональность тестируемого компонента:
import java.util.Stack;
class StackTest {
public static void testStack() {
Stack<Integer> stack = new Stack<>();
// Добавляем два элемента в стек и затем извлекаем их
stack.push(1);
stack.push(2);
// pop() возвращает последнее значение из стека
// и удаляет его оттуда
assertThat(stack.pop()).isEqualTo(2);
assertThat(stack.pop()).isEqualTo(1);
}
}
Этот тест проверяет, что правильно работают два основных метода без учёта пограничных случаев. Для этого внутри теста выполняются два матчера, которые по очереди проверяют извлекаемые значения из стека.
Тестируем дополнительную функциональность
Следующим тестом будет тест на дополнительные методы стека. К таким у нас относится метод isEmpty()
, который проверяет, пустой ли стек:
import java.util.Stack;
class StackTest {
public static void testStackIsEmpty() {
Stack<Integer> stack = new Stack<>();
assertThat(stack.isEmpty()).isTrue();
stack.push(1);
assertThat(stack.isEmpty()).isFalse();
stack.pop();
assertThat(stack.isEmpty()).isTrue();
}
}
В этом тесте проверяются сразу три ситуации:
- начальное состояние стека
- состояние стека после добавления элементов
- состояние стека после извлечения всех элементов
В принципе, этого достаточно. Хотя в теории возможны ситуации, при которых isEmpty()
всё равно сломается. Нужно ли пытаться найти все варианты? Не нужно. Тесты не даются бесплатно, каждая написанная строчка кода в проекте — потенциальное место для изменения в случае правок. Если есть сомнения, нужно ли писать проверку или нет, то лучше не пишите. Так вы поймёте тот минимум, который стоит писать, и после которого тесты писать не эффективно. Редкие ситуации требуют покрытия тестами только тогда, когда они критичны для работоспособности.
Пограничные случаи
Ну, и последнее, что можно протестировать — поведение метода pop()
, когда в стеке нет ни одного элемента. По задумке, стек выбрасывает исключение EmptyStackException
, если из него попытались взять элемент, когда тот был пустой. В библиотеке AssertJ подобные проверки реализуются следующим образом:
// Основные импорты
import static org.assertj.core.api.Assertions.catchThrowable;
import static org.assertj.core.api.Assertions.assertThat;
import java.util.Stack;
import java.util.EmptyStackException;
class StackTest {
public static void testEmptyPop() {
Stack<Integer> stack = new Stack<>();
// Помещаем пробрасываемое исключение в переменную
var thrown = catchThrowable( // этот метод призван "ловить" исключения
() -> stack.pop() // провоцируем исключительную ситуацию
);
// А теперь проверяем что мы поймали нужное исключение
assertThat(thrown).isInstanceOf(EmptyStackException.class);
}
}
Но не всегда пограничные случаи так легко увидеть. Маловероятно, что любой программист сможет сразу написать все нужные тесты. Если в коде возникла ошибка, для которой не было теста, то сначала напишите тест, который воспроизводит эту ошибку, и затем уже чините её. Только так можно поддерживать достаточный уровень надежности, не превращая разработку в непрерывную починку багов.
Дополнительные материалы
Остались вопросы? Задайте их в разделе «Обсуждение»
Вам ответят команда поддержки Хекслета или другие студенты
Для полного доступа к курсу нужен базовый план
Базовый план откроет полный доступ ко всем курсам, упражнениям и урокам Хекслета, проектам и пожизненный доступ к теории пройденных уроков. Подписку можно отменить в любой момент.