4. Модульное тестирование

Цель работы

Дорисовать остальную сову. Доработать и покрыть тестами приложение.

Проверить, что в случае непрохождения тестов автоматическая сборка завершается со статусом failed.

Цель работы:

Реализовать определение факта пересечения фигур.

Easy

Окружность.

Normal

Окружность, треугольник.

Hard

Окружность, треугольник, полигон.

Общие сведения

Для покрытия тестами приложение декомпозируют на минимальные самодостаточные единицы. В процедурных языках программирования такая единица — функция.

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

Код тестов должен быть написан отдельно от кода приложения. Поэтому для запуска тестов пишется отдельное приложение. Тестовое приложение обычно включает в себя автоматическую регистрацию новых тестов и формирование отчета. Для автоматизации процесса используют специальные библиотеки.

Сборка приложения и тестов

В предыдущей лабораторной работе мы разделили проект на консольное приложение и статическую библиотеку. Артефакт сборки тестов — еще один исполняемый файл, который линкуется со статической библиотекой библиотекой. Таким образом, граф зависимостей для примера hello принимает вид (диаграмма сокращена):

digraph G {
  bgcolor = "#ffffff00";
  rankdir = LR;
  "hello" -> "obj/app/main.o";
  "hello" -> "obj/libhello/libhello.a";
  "hello-test" -> "obj/test/main.o";
  "hello-test" -> "obj/libhello/libhello.a";
}

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

При сборке нужно предусмотреть возможность конфликтов имен объектных файлов. Например, при компиляции файлов src/main.c и test/main.c не должен создаваться один и тот же объектный файл obj/main.o. Один из способов предовращения таких конфликтов — дублирование структуры репозитория в каталоге obj:

obj
|-- src
|   |-- parser.o
|   `-- main.o
`-- test
    |-- parser_test.o
    `-- main.o

Библиотека ctest

Примеры доступны по адресу: https://github.com/bvdberg/ctest.

Файл test/main.c:

#define CTEST_MAIN

#include <ctest.h>

int main(int argc, const char** argv)
{
    return ctest_main(argc, argv);
}

Пример определения теста:

#include <sum.h>

#include <ctest.h>

CTEST(arithmetic_suite, simple_sum)
{
    // Given
    const int a = 1;
    const int b = 2;

    // When
    const int result = sum(a, b);

    // Then
    const int expected = 3;
    ASSERT_EQUAL(expected, result);
}

Здесь:

  1. CTEST(<suite_name>, <test_name>) — макрос для создания и регистрации тестовой функции. Все определенные таким образом тесты запускаются автоматически при вызове ctest_main.

  2. ASSERT_EQUAL(<expected>, <real>) — макрос для сравнения ожидаемого результата с фактическим.

Библиотека ctest предоставляет следующие макросы:

  • CTEST(sname, tname) — макрос для определения теста. sname — имя набора тестов, tname — имя теста.

  • CTEST_LOG(const char* fmt, ...) — запись в лог. Используется синтаксис аналогичный printf.

  • ASSERT_STR(exp, real) — макрос для сравнения строк (``char* ``).

  • ASSERT_EQUAL(exp, real) — сравнение переменных типа int.

  • ASSERT_EQUAL_U(exp, real) — сравнение переменных типа unsigned.

  • ASSERT_NOT_EQUAL(exp, real) — проверка на неравенство переменных типа int.

  • ASSERT_NOT_EQUAL_U(exp, real) — проверка на неравенство переменных типа unsigned.

  • ASSERT_DATA(exp, expsize, real, realsize) — сравнение массивов.

  • ASSERT_INTERVAL(exp1, exp2, real) — проверка принадлежности числа real интервалу [exp1, exp2]. Все аргументы типа int

  • ASSERT_DBL_NEAR_TOL(exp, real, tol) — сравнение переменных типа double с заданным допустимым отклонением tol.

  • ASSERT_DBL_NEAR(exp, real) — сравнение переменных типа double с tol = 1e-4

  • ASSERT_NULL(real)

  • ASSERT_NOT_NULL(real)

  • ASSERT_TRUE(real)

  • ASSERT_FALSE(real)

Руководство

В структуру проекта, сформированную в предыдущей лабораторной работе, следует добавить каталоги test и thirdparty. В результате получим:

.
|-- bin
|   |-- geometry
|   `-- geometry-test
|-- .clang-format
|-- .gitignore
|-- Makefile
|-- obj
|-- README.md
|-- src
|-- test
|   |-- parser_test.c
|   `-- main.c
`-- thirdparty
    |-- .clang-format
    `-- ctest.h

Для сторонних библиотек создан отдельный конфиг .clang-format. Его содержимое:

---
DisableFormat: true
SortIncludes: false
...

Форматирование исходного кода сторонних библиотек усложняет их поддержку и вендоринг новых версий, а в случае с ctest приводит к появлению предупреждений на этапе компиляции.

Этапы работы

Каждый этап — отдельный коммит.

  1. Добавить в репозиторий библиотеку ctest (заголовочный файл).

  2. Добавить точку входа для запуска тестов — test/main.c. Настроить сборку и запуск тестов (доработать Makefile). Обычно цель по умолчанию используется только для компиляции приложения. Для компиляции и запуска тестов создают отдельную цель test. Таким образом, для полной сборки приложения и запуска тестов нужно выполнить команды:

    make
    make test
    
  3. Написать любой простейший тест, проверить его работоспособность.

  4. Реализовать функциональность в соответствии со своим вариантом.

  5. Покрыть приложение тестами. Каждую группу тестов можно оформить в отдельный коммит.

  6. Настроить запуск тестов в CI.