Большинство тестов на одну и ту же функциональность сильно похожи друг на друга. Особенно в части начальной подготовки данных. В уроке по модульным тестам каждый тест начинался со строчки: Stack<Integer> stack = new Stack<>();
. Это ещё не дублирование, но уже шаг в эту сторону. Как правило, реальные тесты сложнее и включают в себя большую подготовительную работу.
Допустим, мы разрабатываем собственную коллекцию, такую же как ArrayList и хотим протестировать методы для обработки коллекций:
- contains
- get
- remove
- и другие (всего их более 20 штук)
Для работы этих методов нужна заранее подготовленная коллекция. Проще всего придумать одну, которая подойдёт для тестирования большинства или даже всех методов:
import static org.junit.jupiter.api.Assertions.*;
import org.junit.jupiter.api.Test;
import java.util.ArrayList;
public class ArrayListTest {
@Test
public void testGet() {
ArrayList<Integer> list = new ArrayList<>();
list.addAll(Arrays.asList(1, 2, 3, 4, 5, 6));
assertEquals(1, list.get(0));
}
@Test
public void testContains() {
ArrayList<Integer> list = new ArrayList<>();
list.addAll(Arrays.asList(1, 2, 3, 4, 5, 6));
assertTrue(list.contains(5));
}
}
Теперь представьте, что таких тестов несколько десятков (в реальности их сотни). Код с инициализацией и наполнением коллекции начнёт кочевать из одного места в другое, порождая всё больше и больше копипасты. И если мы ещё можем вынести инициализацию коллекции на уровень класса, т.е. сделать её полем, то с наполнением её данными такой "финт" уже не пройдёт. Нам необходимо вынести эти действия в отдельный метод.
public class ArrayListTest {
ArrayList<Integer> list = new ArrayList<>();
public void init () {
// в реальной практике этот метод может содержать много строк кода
list.addAll(Arrays.asList(1, 2, 3, 4, 5, 6));
}
}
Это простое решение убирает ненужное дублирование, хотя сам метод init()
нам по-прежнему нужно вызывать внутри каждого из тестовых методов.
public class ArrayListTest {
@Test
public void testSomeMethod() {
init(); // Запускаем инициализацию
// Сам тест
}
}
Тесты в JUnit обладают одной очень важной особенностью. Посмотрите на код ниже и подумайте одинаковые или разные значения выведут в консоль первый и второй тесты? Вот сам код:
public class TestClass {
private long startTime = System.currentTimeMillis();
@Test
public void testFirst() {
System.out.println("first = " + startTime);
}
@Test
public void testSecond() {
System.out.println("second = " + startTime);
}
}
Подвох тут в том, что переменная startTime
инициализируется далеко не один раз. JUnit
устроен так, что создаёт новый экземпляр класса для каждого теста (метода, помеченного аннотацией @Test
). То есть переменная startTime
будет инициализирована при создании экземпляра для теста testFirst()
, а также при создании собственного экземпляра класса TestClass
для теста testSecond()
. Поэтому между нестатичными полями в разных тестах не будет совершенно никакой связи.
Следующий вопрос - какой из тестов запустится раньше? То есть у которого из тестов значение переменной startTime
будет меньше? Заранее предсказать ответ на этот вопрос практически невозможно. В этом примере сначала запустился тест testSecond()
, а потом запустился тест testFirst()
. Попробуйте запустить подобный пример на своём компьютере и посмотреть на результаты.
Почему такое происходит? Именно потому, что мы отдаём бразды правления тестами фреймворку. Он будет определять оптимальный порядок и способ запуска тестов. Пользователю при этом необходимо написать тесты, которые не будут зависеть друг от друга, но об этом мы поговорим в следующем уроке.
Итак, вернёмся к вопросу о правильной инициализации нашей коллекции перед началом каждого теста. Для решения этой проблемы тестовые фреймворки предоставляют хуки — специальные методы, которые запускаются до или после тестов. Ниже пример того, как наполнить нашу коллекцию перед каждым тестом:
public class ArrayListTest {
ArrayList<Integer> list = new ArrayList<>();
@BeforeEach
public void beforeEach() {
list.addAll(Arrays.asList(1, 2, 3, 4, 5, 6));
}
@Test
public void testGet() {
assertEquals(1, list.get(0));
}
@Test
public void testContains() {
assertTrue(list.contains(5));
}
}
Аннотацией @BeforeEach
помечаются методы, которые будут выполняться перед стартом каждого из тестовых методов. В этих методах не обязательно создаются переменные. Возможно, инициализация заключается в подготовке файловой системы, например, созданию файлов. Но если метод должен создать данные и сделать их доступными в тестах, то придётся использовать поля класса.
Если нам нужно выполнить код один раз перед всеми тестами, его нужно выполнять внутри метода с аннотацией @BeforeAll
. Этот хук запускается ровно один раз перед всеми тестами, расположенными в одном классе. При этом сам метод, помеченный аннотацией @BeforeAll
должен быть уже статичным.
@BeforeAll
public static void beforeAll() {
// Делаем тут какую-то подготовку
}
Аналогично, существуют аннотации @AfterEach
и @AfterAll
, которые позволяют выполнить определённые действия после каждого или после всех тестов. Например, вы можете написать метод, который удалит созданный в начале файл.
Почему важно использовать аннотации, а не самостоятельно организовывать порядок и способ выполнения тестов? Самый простой ответ на этот вопрос такой: JUnit должен контролировать происходящие процессы и побочные эффекты в тестах. Все, что вызывается вами вручную, отрабатывается вне JUnit. Это значит, что JUnit никак не может отследить, что происходит, и в какой момент можно запускать тесты.
Остались вопросы? Задайте их в разделе «Обсуждение»
Вам ответят команда поддержки Хекслета или другие студенты
Для полного доступа к курсу нужен базовый план
Базовый план откроет полный доступ ко всем курсам, упражнениям и урокам Хекслета, проектам и пожизненный доступ к теории пройденных уроков. Подписку можно отменить в любой момент.