1. Ход работы

Quality Assurance (QA) – это совокупность мероприятий, охватывающих все технологические этапы разработки, выпуска и эксплуатации программного обеспечения для обеспечения требуемого уровня качества выпускаемого продукта.

Тестирование – проверка соответствия между реальным и ожидаемым поведением программы, осуществляемая на конечном наборе тестов, выбранном определенным образом. Тестирование – это одна из техник контроля качества, включающая в себя планирование, проектирование, выполнение тестирования и анализ результатов.

Тестовый случай (test case, «тест-кейс») – совокупность шагов, условия и параметров, необходимых для проверки реализации тестируемой функции или ее части.

Тестовое покрытие (Test Coverage) – одна из метрик оценки качества тестирования. Она показывает процент исходного кода программы, который был выполнен в процессе тестирования.

Автоматизированное тестирование ПО – это процесс верификации ПО, при котором основные функции и шаги теста (запуск, инициализация, выполнение, анализ и выдача результата) выполняются автоматически при помощи инструментов для автоматизированного тестирования.

Unit-тестирование (Модульное тестирование)

Что такое «unit»?

Unit – это маленький самодостаточный участок кода, реализующий определенное поведение. Это может быть класс, набор функций, даже несколько маленьких классов.

Unit-тестирование позволяет выявить ошибки на небольших участках кода и показывает, что отдельные небольшие участки кода работают корректно.

Идея состоит в том, чтобы писать тесты для каждой нетривиальной функции или метода. Это позволяет достаточно быстро проверить, не привело ли очередное изменение кода к регрессии, то есть к появлению ошибок в уже оттестированных местах программы, а также облегчает обнаружение и устранение таких ошибок.

Любой долгосрочный проект без надлежащего покрытия тестами обречен рано или поздно быть переписанным с нуля

Тестируйте одну вещь за один раз

Каждый тест должен проверять только одну вещь. Если процесс слишком сложен (например, покупка в интернет магазине), разделите его на несколько частей и протестируйте их отдельно. Если вы не будете придерживаться этого правила, ваши тесты станут нечитаемыми, и вскоре вам окажется очень сложно их поддерживать.

Тесты как документация

Unit-тесты могут служить в качестве документации к коду. Грамотный набор тестов, который покрывает возможные способы использования, ограничения и потенциальные ошибки, ничуть не хуже специально написанных примеров, и, кроме того, его можно скомпилировать и убедиться в корректности реализации.

Возможность лучше разобраться в коде

Когда вы разбираетесь в плохо документированном сложном старом коде, попробуйте написать для него тесты. Это может быть непросто, но достаточно полезно, так как:

  • они позволят вам убедиться, что вы правильно понимаете, как работает код;

  • они будут служить документацией для тех, кто будет читать код после вас;

  • если вы планируете рефакторинг, тесты помогут вам убедиться в корректности изменений.

Библиотека JUnit

JUnit – библиотека для unit-тестирования на java.

Создадим в Intellij IDEA новый проект. Создадим класс Item, со строковыми полями name и price, а также заготовку для класса ItemStack, который будет реализовывать стек с помощью массива.

ItemStack.java
public class ItemStack {

    private Item[] array;
    private int pointer;

    public ItemStack(int capacity) {
        array = new Item[capacity];
        pointer = 0;
    }

    public void addItem(Item item) {
    }
    
    public Item getItem() {
        return null;
    }
    
    public boolean isEmpty() {
        return false;
    }
    
    public boolean isFull() {
        return false;
    }
}

Создадим папку test и пометим ее как Test Sources Root

Перейдем в класс, который мы хотим протестировать (в данном случае это класс ItemStack) и создадим новый тест для этого класса (комбинация клавиш Alt + Insert)

В появившемся меню выбираем JUnit5 в качестве Testing library, после чего нажимаем кнопку Fix для того чтобы скачать и подключить требуемые библиотеки

В результате этих действий для нас будет создан тестовый класс ItemStackTest.

Тестовый класс – это специальный класс, предназначенный для тестирования. Каждый тест-кейс – это метод тестового класса.

В тестовом методе пишется код, который позволяет получить от проверяемого юнита какое-то значение. Каждый выполняемый тест должен быть независимым от других тестов, так как порядок выполнения тестовых методов не определен.

Логика работы тестового метода следующая - тестовый метод запускается JUnit для исполнения. Если метод отработал без ошибок - тест считается пройденным. Если в результате работы метода было выброшено исключение - тест считается проваленным.

Напишем два тестовых метода - один гарантированно успешный, а другой гарантированно провальный. Тестовый метод помечается аннотацией @Test. Запустим проведение тестов и посмотрим на результат

Существует общепринятый порядок написания unit-тестов, который называется AAA:

  • Arrange – создание тестовых объектов и подготовка к тесту;

  • Act – непосредственная работа с тестируемым кодом;

  • Assert – проверка результата.

Логика работы тестового метода следующая: если unit работает корректно, то его ожидаемое поведение должно совпадать с его реальным поведением. Таким образом, мы создаем нужные объекты, вызываем метод или методы для тестирования юнита, после чего сравниваем фактический результат работы юнита с ожидаемым результатом.

Для сравнения ожидаемого и реального поведения юнита используются методы assert. Если результаты не совпадают, метод assert выбросит исключение. Таким образом, тест считается проваленным, если реальное поведение юнита отличается от его фактического поведения.

Давайте напишем тестовый метод с использованием этого принципа. Напишем код для класса ItemStack, после чего напишем тестовый метод для проверки того, был ли добавлен Item в стек или нет.

ItemStackTest.java
class ItemStackTest {

    @Test
    public void testConstructor() {
        // Arrange - создаем тестовый объект
        ItemStack stack = new ItemStack(10);
        Item item = new Item("Ручка",20.50);

        // Act - вызов тестируемого кода
        stack.addItem(item);
        boolean result = stack.isEmpty();

        // Assert - сравнение ожидаемого поведения и реального
        assertFalse(result);
    }
}

Метод assertFalse() проверяет, является ли аргумент метода равным false. Если бы result был равен true, тогда бы метод assertFalse() выбросил исключение и тест был бы провален.

Грамотные unit-тесты должны соответствовать принципам FIRST:

  • Fast – unit-тесты должны быть быстрыми (следует избегать длительных операций);

  • Isolates – изолирование ошибки. Если тест провален – причина должна быть сразу понятна. Избегайте слишком сложных тестов;

  • Repeateable – тесты должны быть повторяемыми в любом порядке. Они не должны зависеть от конкретного начального состояния и не должны оставлять после себя изменения, которые могу повлиять на последующее тестирование;

  • Self-validating – тест либо пройден, либо провален;

  • Timely – тесты должны быть написаны в правильное время, сразу после того как был написан код для тестирования.

Изначально тестируется т.н. Happy Path – тестирование нормального поведения юнита (ввод правильных данных, правильные условия использования юнита и тд).

Кроме аннотации @Test существует еще множество аннотаций

Про аннотации JUnit5 и сценарии их использования можно прочесть здесь.

Тестирование исключений

На примере ниже показано два способа тестирования исключений. В первом случае проверяется также сообщение при выборе исключения, во второй случае проверяется только класс исключения.

@Test
void shouldThrowException() {
    Throwable exception = assertThrows(UnsupportedOperationException.class, () -> {
        throw new UnsupportedOperationException("Not supported");
    });
    assertEquals(exception.getMessage(), "Not supported");
}

@Test
void assertThrowsException() {
    String str = null;
    assertThrows(IllegalArgumentException.class, () -> {
        Integer.valueOf(str);
    });
}

Для проверки граничных условий применяют принцип CORRECT:

  • Conformance (Соответствие) – соответствует ли значение ожидаемому формату?

  • Ordering (Порядок) – находится ли набор значений в нужном порядке или нужно, чтобы он был непорядочен?

  • Range (Диапазон) – Находится ли значение в рамках разумного минимального и максимального значения?

  • Reference — Ссылается ли тестируемый код на какой-то внешний объект, который кодом не контролируется?

  • Existence — Существует ли значение (не-null, не нулевое, находится во множестве). Как ведет себя юнит при 0 элементов, при 1 элементе, при множестве элементов?

  • Cardinality — Количество значений точно необходимое?

  • Time (absolute and relative) — Все происходит в нужном порядке и в нужное время? Вовремя?

Разработка через тестирование (TDD, test-driven development). Сначала пишутся тесты, которые проверяют поведение кода. После этого пишется код, который проходит эти тесты.

Last updated