> For the complete documentation index, see [llms.txt](https://opu.gitbook.io/oop2/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://opu.gitbook.io/oop2/practice-5/example.md).

# 1. Ход работы

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

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

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

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

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

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

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

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

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

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

{% hint style="danger" %}
Любой долгосрочный проект без надлежащего покрытия тестами обречен рано или поздно быть переписанным с нуля
{% endhint %}

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

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

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

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

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

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

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

## Библиотека JUnit

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

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

{% code title="ItemStack.java" %}

```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;
    }
}
```

{% endcode %}

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

![](https://156292728-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-LZaTGrFtB1bdGKXJN28%2F-LdAIuT8WJTF-DVrCtsT%2F-LdAJB_rdhT6uivUveBj%2Fassets_-LIEGYndXIZn8whSYtWl_-LcW6gD2HtL9jpDf0N6N_-LcWKKH7W3AkeKgnhtnK_image.png?alt=media\&token=314c7a3c-bc6b-4883-a84d-c8c99cc69b30)

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

![](https://156292728-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-LZaTGrFtB1bdGKXJN28%2F-LdAIuT8WJTF-DVrCtsT%2F-LdAJE_4yqSEv4JHoIt4%2Fassets_-LIEGYndXIZn8whSYtWl_-LcW6gD2HtL9jpDf0N6N_-LcWKZmCtV8GWJWcieyt_image.png?alt=media\&token=3b8e068c-58c3-48e2-af60-f41ff2b7a0ee)

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

![](https://156292728-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-LZaTGrFtB1bdGKXJN28%2F-LdAIuT8WJTF-DVrCtsT%2F-LdAJH9PLzyQgc-JdM0q%2Fassets_-LIEGYndXIZn8whSYtWl_-LcW6gD2HtL9jpDf0N6N_-LcWKkSc1S3KuCDlzHfc_image.png?alt=media\&token=9788414e-e15f-47fe-9414-f0ab9a170921)

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

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

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

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

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

![](https://156292728-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-LZaTGrFtB1bdGKXJN28%2F-LdAIuT8WJTF-DVrCtsT%2F-LdAJKHAD9or8MKuuudO%2Fassets_-LIEGYndXIZn8whSYtWl_-LcW6gD2HtL9jpDf0N6N_-LcWNePwPKDjv74U2ReN_image.png?alt=media\&token=dd47c98a-212a-4133-bbcd-87c16d26b157)

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

* **Arrange** – создание тестовых объектов и подготовка к тесту;
* **Act** – непосредственная работа с тестируемым кодом;
* **Assert** – проверка результата.

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

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

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

{% code title="ItemStackTest.java" %}

```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);
    }
}
```

{% endcode %}

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

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

* **Fast** – unit-тесты должны быть быстрыми (следует избегать длительных операций);
* **Isolates** – изолирование ошибки. Если тест провален – причина должна быть сразу понятна. Избегайте слишком сложных тестов;
* **Repeateable** – тесты должны быть повторяемыми в любом порядке. Они не должны зависеть от конкретного начального состояния и не должны оставлять после себя изменения, которые могу повлиять на последующее тестирование;
* **Self-validating** – тест либо пройден, либо провале&#x43D;**;**
* **Timely** – тесты должны быть написаны в правильное время, сразу после того как был написан код для тестирования.

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

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

![](https://156292728-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-LZaTGrFtB1bdGKXJN28%2F-LdAIuT8WJTF-DVrCtsT%2F-LdAJNnrhl3xgTzybiwF%2Fassets_-LIEGYndXIZn8whSYtWl_-Lbxx6JA5gmy8EhP2n7a_-LbxyEen0UOu5sU9j6d4_image.png?alt=media\&token=62b49e48-05fe-4ad6-ad10-f2279bf21725)

{% hint style="info" %}
Про аннотации JUnit5 и сценарии их использования можно прочесть [здесь](https://www.testingexcellence.com/junit-5-annotations/).
{% endhint %}

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

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

```java
@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). Сначала пишутся тесты, которые проверяют поведение кода. После этого пишется код, который проходит эти тесты.
