Skip to the content.

Методика тестирования

При разработке программного обеспечения применяется гибридная стратегия тестирования, сочетающая system testing / end-to-end (E2E) testing, элементы integration testing, а также практику contract testing и cross-checking (differential-style testing) между двумя путями исполнения.

Каждый тестовый сценарий выполняется в двух режимах (dual-path execution):

  1. In-process harness test: точка входа приложения (main) переименовывается/подменяется и вызывается внутри процесса тестового фреймворка. Аргументы передаются в том же формате, как если бы они пришли из командной строки (argv/argc semantics). Этот режим даёт быстрый прогон логики в контролируемом окружении и более точную локализацию ошибок.

  2. Out-of-process black-box CLI test: те же аргументы передаются реальному скомпилированному бинарнику, который запускается как отдельный процесс (fork/spawn) из тестового фреймворка. Это соответствует чёрноящичному системному/E2E тесту, максимально близкому к реальному сценарию использования CLI.

Для обоих режимов применяются одинаковые оракулы (test oracles) и проверки:

Сценарий считается корректным, когда наблюдаемое поведение согласуется в обоих режимах, что используется как cross-checking: результаты «in-process» и «black-box» прогона должны соответствовать одному и тому же контракту вывода и ожидаемому состоянию данных.

Дополнительно тестовый контур усиливается динамическим анализом: и тестовый фреймворк, и тестируемый код компилируются и исполняются с санитайзерами (AddressSanitizer (ASan) и UndefinedBehaviorSanitizer (UBSan)). Это означает, что проверки на утечки памяти, ошибки доступа к памяти и проявления undefined behavior выполняются как для in-process прогона, так и out-of-process при запуске отдельного процесса (runtime bug detection / dynamic analysis).

Отдельным этапом выполняется анализ test coverage (line coverage и branch coverage). По результатам анализа строится HTML coverage report, используемый как артефакт качества и навигации по покрытию нового или изменённого кода.

А поговорить?

Почему мне не нравится полагаться только на юнит-тесты. Они действительно помогают быстро проверить отдельные функции и ветки, но сами по себе не дают гарантии, что эта логика так же отработает в реальной программе — с настоящими аргументами командной строки, окружением, вводом/выводом и побочными эффектами. Бывает, что в юнит-тесте ветка выполняется и тест зелёный, а в реальном запуске до этой ветки просто не дойти из-за другой логики или из-за отличий в контексте выполнения.

Поэтому основной упор делается на системные/интеграционные (E2E/CLI) тесты, которые запускают программу так, как её запускает пользователь. Такие тесты лучше подтверждают, что нужные ветки реально выполняются в «боевом» сценарии, а анализ покрытия (особенно branch coverage) помогает увидеть участки кода, которые ни разу не исполнились. Это сигнал: либо тестами не покрыт важный сценарий, либо код лишний/недостижимый и его стоит пересмотреть.

При этом важно смотреть покрытие по наборам тестов отдельно. Если считать покрытие суммарно по всем тестам сразу, юнит-тесты могут «закрыть» строки и ветки, которые интеграционные/E2E тесты так ни разу и не затронули — и это легко пропустить. Раздельная статистика даёт честную картину: что проверено именно реальными прогонами, а что — только в изоляции.

Юнит-тесты при этом не отвергаются. Они полезны там, где нужно быстро и точно проверить маленький кусок логики, крайние случаи и редкие ошибки, которые сложно или дорого воспроизвести интеграционно (в том числе через моки/фейки). Просто смысл в том, чтобы каждый уровень тестов добавлял свою пользу, а не дублировал один и тот же сценарий без необходимости.