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

Godot Engine позволяет писать модульные тесты непосредственно на C++. Движок интегрирует фреймворк модульного тестирования doctest, который позволяет писать тестовые наборы и тестовые случаи рядом с производственным кодом. Но поскольку тесты в Godot проходят через другую точку входа main, они находятся в выделенном каталоге tests/, расположенном в корне исходного кода движка.

Поддержка платформы и цели

Модульные тесты C++ можно запускать в операционных системах Linux, macOS и Windows.

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

Проведение тестов

Перед тем как тесты можно будет запустить, движок должен быть скомпилирован с включенной опцией сборки tests (и любой другой опцией сборки, которую вы обычно используете), поскольку тесты по умолчанию не компилируются как часть движка:

scons tests=yes

После завершения сборки запустите тесты с помощью параметра командной строки --test:

./bin/<godot_binary> --test

Тестовый запуск можно настроить с помощью различных параметров командной строки, специфичных для doctest. Чтобы получить полный список поддерживаемых параметров, выполните команду --test с параметром --help:

./bin/<godot_binary> --test --help

Любые другие параметры и аргументы после команды --test рассматриваются как аргументы для doctest.

Примечание

Тесты компилируются автоматически, если используется опция SCons dev_mode=yes. Рекомендуется использовать dev_mode=yes, если вы планируете участвовать в разработке движка, так как в этом случае предупреждения компиляции будут автоматически обрабатываться как ошибки. Система непрерывной интеграции завершится сбоем при обнаружении любых предупреждений компиляции, поэтому перед открытием запроса на извлечение постарайтесь исправить все предупреждения.

Фильтрационные тесты

По умолчанию все тесты запускаются, если после команды --test не указаны дополнительные аргументы. Но если вы пишете новые тесты или хотите просмотреть результаты успешных утверждений, полученных в результате этих тестов, для отладки, вы можете запустить интересующие вас тесты с помощью различных параметров фильтрации, предоставляемых doctest.

Подстановочный синтаксис * поддерживается для сопоставления любого количества символов в тестовых наборах, тестовых случаях и именах исходных файлов:

Параметры фильтра

Стенография

Примеры

--test-suite

-ts

-ts="*[GDScript]*"

--test-case

-tc

-tc="*[String]*"

--source-file

-sf

-sf="*test_color*"

Например, чтобы запустить только модульные тесты String, выполните:

./bin/<godot_binary> --test --test-case="*[String]*"

Вывод успешных утверждений можно включить с помощью параметра --success (-s) и можно объединить с любой комбинацией приведенных выше параметров фильтрации, например:

./bin/<godot_binary> --test --source-file="*test_color*" --success

Отдельные тесты можно пропустить с помощью соответствующих опций -exclude. В настоящее время некоторые тесты включают случайные стресс-тесты, выполнение которых занимает некоторое время. Чтобы пропустить такие тесты, выполните следующую команду:

./bin/<godot_binary> --test --test-case-exclude="*[Stress]*"

Написание тестов

Тестовые наборы представляют собой заголовочные файлы C++, которые должны быть включены в основную точку входа теста в tests/test_main.cpp. Большинство тестовых наборов находятся непосредственно в каталоге tests/.

Все заголовочные файлы имеют префикс test_, и это соглашение об именовании, на которое опирается система сборки Godot для обнаружения тестов во всем движке.

Вот минимальный рабочий набор тестов с одним написанным тестовым случаем:

#pragma once

#include "tests/test_macros.h"

namespace TestString {

TEST_CASE("[String] Hello World!") {
    String hello = "Hello World!";
    CHECK(hello == "Hello World!");
}

} // namespace TestString

Примечание

Вы можете быстро создавать новые тесты с помощью скрипта create_test.py, находящегося в каталоге tests/. Этот скрипт автоматически создаёт новый тестовый файл с необходимым шаблонным кодом в соответствующем месте. Он также может автоматически включать новый заголовок в tests/test_main.cpp, используя инвазивный режим (флаг -i). Чтобы просмотреть инструкции по использованию, запустите скрипт с флагом -h.

Заголовочный файл tests/test_macros.h содержит всё необходимое для написания модульных тестов C++ в Godot. Он включает в себя макросы doctest-утверждения и протоколирования, такие как CHECK, как показано выше, и, конечно же, определения для написания самих тестовых случаев.

См. также

tests/test_macros.h исходный код для текущих реализованных макросов и псевдонимов для них.

Тестовые случаи создаются с помощью функционального макроса TEST_CASE. Каждый тестовый случай должен иметь краткое описание в скобках, при необходимости включая пользовательские теги, позволяющие фильтровать тесты во время выполнения, например [String], [Stress] и т. д.

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

Godot поддерживает написание тестов для каждого модуля C++. Инструкции по написанию модульных тестов см. в документе Написание пользовательских модульных тестов.

Subcases (Подслучаи)

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

TEST_CASE("[SceneTree][Node] Testing node operations with a very simple scene tree") {
    // ... common setup (e.g. creating a scene tree with a few nodes)
    SUBCASE("Move node to specific index") {
        // ... setup and checks for moving a node
    }
    SUBCASE("Remove node at specific index") {
        // ... setup and checks for removing a node
    }
}

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

Assertions (Утверждения)

Список всех часто используемых утверждений, используемых в тестах Godot, отсортированных по степени серьезности.

Assertion

Описание

REQUIRE

Проверяет, выполняется ли условие. Если условие не выполняется, тест немедленно проваливается.

REQUIRE_FALSE

Проверка на невыполнение условия. Если условие выполняется, тест немедленно проваливается.

CHECK

Проверяет, выполняется ли условие. Отмечает выполнение теста как неудавшееся, но разрешает выполнение других утверждений.

CHECK_FALSE

Проверка, выполняется ли условие. Отмечает выполнение теста как неудавшееся, но разрешает выполнение других утверждений.

WARN

Проверяет, выполняется ли условие. Тест не проваливается ни при каких обстоятельствах, но выводит предупреждение, если что-то не выполняется.

WARN_FALSE

Проверка на невыполнение условия. Не проваливает тест ни при каких обстоятельствах, но выводит предупреждение, если условие выполняется.

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

Предпочитайте использовать CHECK для не требующих пояснений утверждений и CHECK_MESSAGE для более сложных, если вы считаете, что они заслуживают лучшего объяснения.

Ведение журнала

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

Macro

Описание

MESSAGE

Печатает сообщение.

FAIL_CHECK

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

FAIL

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

Различные источники отчётов можно выбирать во время выполнения. Например, вот как можно перенаправить вывод в XML-файл:

./bin/<godot_binary> --test --source-file="*test_validate*" --success --reporters=xml --out=doctest.txt

Тестирование путей отказа

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

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

Чтобы решить эту проблему, используйте макросы ERR_PRINT_OFF и ERR_PRINT_ON непосредственно в тестовых случаях, чтобы временно отключить вывод ошибок, поступающих от движка, например:

TEST_CASE("[Color] Constructor methods") {
    ERR_PRINT_OFF;
    Color html_invalid = Color::html("invalid");
    ERR_PRINT_ON; // Don't forget to re-enable!

    CHECK_MESSAGE(html_invalid.is_equal_approx(Color()),
        "Invalid HTML notation should result in a Color with the default values.");
}

Специальные теги в названиях тестовых случаев

Эти теги можно добавить к имени тестового случая, чтобы изменить или расширить тестовую среду:

Tag

Описание

[SceneTree]

Требуется для тестовых случаев, требующих наличия дерева сцены с MessageQueue. Также включает фиктивный сервер рендеринга и ThemeDB.

[Editor]

Подобно [SceneTree], но с дополнительной доступной инфраструктурой, связанной с редактором, например EditorSettings.

[Audio]

Инициализирует AudioServer, используя фиктивный аудиодрайвер.

[Navigation2D]

Создает сервер 2D-навигации по умолчанию и делает его доступным для тестирования.

[Navigation3D]

Создает сервер 3D-навигации по умолчанию и делает его доступным для тестирования.

Вы можете использовать их вместе для объединения нескольких расширений тестовой среды.

Тестовые сигналы

Для проверки сигналов можно использовать следующие макросы:

Macro (Макрос)

Описание

SIGNAL_WATCH(object, "signal_name")

Начинает отслеживать указанный сигнал на указанном объекте.

SIGNAL_UNWATCH(object, "signal_name")

Останавливает наблюдение за указанным сигналом на указанном объекте.

SIGNAL_CHECK("signal_name", Vector<Vector<Variant>>)

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

SIGNAL_CHECK_FALSE("signal_name")

Проверяет, не был ли сгенерирован указанный сигнал.

SIGNAL_DISCARD("signal_name")

Удаляет все записи указанного сигнала.

Ниже приведен пример, демонстрирующий использование этих макросов:

//...
SUBCASE("[Timer] Timer process timeout signal must be emitted") {
    SIGNAL_WATCH(test_timer, SNAME("timeout"));
    test_timer->start(0.1);

    SceneTree::get_singleton()->process(0.2);

    Array signal_args;
    signal_args.push_back(Array());

    SIGNAL_CHECK(SNAME("timeout"), signal_args);

    SIGNAL_UNWATCH(test_timer, SNAME("timeout"));
}
//...

Инструменты для тестирования

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

Эти инструменты можно запустить, указав их название после параметра командной строки --test. Например, модуль GDScript реализует и регистрирует несколько инструментов для отладки токенизатора, парсера и компилятора:

./bin/<godot_binary> --test gdscript-tokenizer test.gd
./bin/<godot_binary> --test gdscript-parser test.gd
./bin/<godot_binary> --test gdscript-compiler test.gd

Если обнаружен какой-либо такой инструмент, то остальные модульные тесты пропускаются.

Тестовые инструменты можно регистрировать в любом месте движка, поскольку механизм регистрации очень похож на тот, что предоставляет doctest при регистрации тестовых случаев с использованием техники динамической инициализации, но обычно их можно регистрировать в соответствующих источниках register_types.cpp (для модуля или ядра).

Вот пример того, как GDScript регистрирует тестовые инструменты в modules/gdscript/register_types.cpp:

#ifdef TESTS_ENABLED
void test_tokenizer() {
    TestGDScript::test(TestGDScript::TestType::TEST_TOKENIZER);
}

void test_parser() {
    TestGDScript::test(TestGDScript::TestType::TEST_PARSER);
}

void test_compiler() {
    TestGDScript::test(TestGDScript::TestType::TEST_COMPILER);
}

REGISTER_TEST_COMMAND("gdscript-tokenizer", &test_tokenizer);
REGISTER_TEST_COMMAND("gdscript-parser", &test_parser);
REGISTER_TEST_COMMAND("gdscript-compiler", &test_compiler);
#endif

Пользовательский разбор командной строки может быть выполнен самим тестовым инструментом с помощью метода ОС get_cmdline_args.

Интеграционные тесты для GDScript

Godot использует doctest для предотвращения регрессий в GDScript во время разработки. Существует несколько типов тестовых скриптов:

  • тесты на ожидаемые ошибки;

  • тесты на предупреждения;

  • тесты на характеристики.

Таким образом, процесс написания интеграционных тестов для GDScript выглядит следующим образом:

  1. Выберите тип тестового скрипта, который вы хотите написать, и создайте новый файл GDScript в каталоге modules/gdscript/tests/scripts в соответствующем подкаталоге.

  2. Напишите код GDScript. Тестовый скрипт должен содержать функцию test(), не принимающую аргументов. Эта функция будет вызываться исполнителем тестов. Тест не должен иметь никаких зависимостей, если только он сам не является частью теста. Глобальные классы (использующие class_name) регистрируются до запуска исполнителя тестов, поэтому они должны работать при необходимости.

    Вот пример тестового сценария:

    func test():
        if true # Missing colon here.
            print("true")
    
  3. Измените каталог на корень исходного репозитория Godot.

    cd godot
    
  4. Сгенерируйте файлы *.out для обновления ожидаемых результатов вывода:

    bin/<godot_binary> --gdscript-generate-tests modules/gdscript/tests/scripts
    

Вы можете добавить опцию --print-filenames, чтобы видеть имена файлов по мере генерации результатов их тестирования. Если вы работаете над новой функцией, которая вызывает серьёзные сбои, вы можете использовать эту опцию, чтобы быстро найти тестовый файл, вызывающий сбой, и выполнить отладку на его основе.

  1. Запустите тесты GDScript с помощью:

    ./bin/<godot_binary> --test --test-suite="*GDScript*"
    

Также принимается опция --print-filenames (см. выше).

Если никаких ошибок не обнаружено и все прошло успешно, все готово!

Предупреждение

Перед отправкой запроса на включение изменений убедитесь, что выходные данные содержат ожидаемые значения. Если --gdscript-generate-tests создаёт файлы *.out, не связанные с новыми добавленными тестами, следует вернуть эти файлы и коммитить только файлы *.out для новых тестов.

Примечание

Test runner GDScript предназначен для тестирования реализации GDScript, а не для тестирования пользовательских скриптов или движка с их помощью. Мы рекомендуем написать новые тесты для уже решённых проблем, связанных с GDScript, на GitHub <https://github.com/godotengine/godot/issues?q=is%3Aissue+label%3Atopic%3Agdscript+is%3Aclosed> или для уже работающих функций.

Примечание

Если ваш тестовый случай требует отсутствия функции test() в файле скрипта, вы можете отключить исполняемую часть теста, назвав файл скрипта так, чтобы он соответствовал шаблону *.notest.gd. Например, "test_empty_file.notest.gd".