Зарегистрируйтесь, чтобы продолжить обучение

Подготовка данных Java: Автоматическое тестирование

Большинство тестов на одну и ту же функциональность сильно похожи друг на друга. Особенно в части начальной подготовки данных. В уроке по модульным тестам каждый тест начинался со строчки: 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 никак не может отследить, что происходит, и в какой момент можно запускать тесты.


Аватары экспертов Хекслета

Остались вопросы? Задайте их в разделе «Обсуждение»

Вам ответят команда поддержки Хекслета или другие студенты

Для полного доступа к курсу нужен базовый план

Базовый план откроет полный доступ ко всем курсам, упражнениям и урокам Хекслета, проектам и пожизненный доступ к теории пройденных уроков. Подписку можно отменить в любой момент.

Получить доступ
1000
упражнений
2000+
часов теории
3200
тестов

Открыть доступ

Курсы программирования для новичков и опытных разработчиков. Начните обучение бесплатно

  • 130 курсов, 2000+ часов теории
  • 1000 практических заданий в браузере
  • 360 000 студентов
Отправляя форму, вы принимаете «Соглашение об обработке персональных данных» и условия «Оферты», а также соглашаетесь с «Условиями использования»

Наши выпускники работают в компаниях:

Логотип компании Альфа Банк
Логотип компании Aviasales
Логотип компании Yandex
Логотип компании Tinkoff
Рекомендуемые программы
профессия
Программирование на Java, Разработка веб-приложений и микросервисов используя Spring Boot, проектирование REST API
10 месяцев
с нуля
Старт 26 декабря

Используйте Хекслет по-максимуму!

  • Задавайте вопросы по уроку
  • Проверяйте знания в квизах
  • Проходите практику прямо в браузере
  • Отслеживайте свой прогресс

Зарегистрируйтесь или войдите в свой аккаунт

Отправляя форму, вы принимаете «Соглашение об обработке персональных данных» и условия «Оферты», а также соглашаетесь с «Условиями использования»