← almazrobotsэталонный разбор · тестирование
Эталонный разбор · культура тестирования
590×

столько тестового кода приходится на каждую строку рабочего кода SQLite

Как тестируют продукт, который
нельзя позволить себе сломать

SQLite — самая распространённая СУБД в мире: миллиарды устройств, браузеры, телефоны, самолёты, и даже техника за пределами Земли. Такая надёжность держится не на удаче, а на одной из самых дисциплинированных систем тестирования в индустрии. Этот документ разбирает её по уровням — как универсальный ориентир, к которому может стремиться любая продуктовая команда.

590×
тестового кода к рабочему
4
независимых стенда
100%
покрытие ветвей + MC/DC ядра
~248 млн
тестов в soak- прогоне релиза
~500 млн
fuzz-кейсов в день
01 · почему так много

Надёжность — это не обещание, а побочный продукт процесса

Девиз SQLite — «Маленькая. Быстрая. Надёжная. Выберите любые три». Третье свойство стоит на простой и неудобной мысли: база данных помнит ошибки.

Упавший сервер можно перезапустить. Повреждённую запись — нет. Стоимость дефекта в СУБД асимметрична: ошибка на проде не просто роняет процесс, она тихо портит данные, которые будут читать годами. Поэтому всё тестирование выстроено вокруг одной цели — не дать дефекту дожить до релиза.

Из этого следует второй принцип. Легко написать движок, который работает на корректных входных данных и исправном железе. Трудно — тот, что ведёт себя предсказуемо, когда заканчивается память, отказывает диск, пропадает питание посреди записи или на вход прилетает враждебный мусор. Большая часть усилий направлена именно на это «когда всё ломается».

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

Важная оговорка — читать до того, как копировать

Сами авторы SQLite прямо предупреждают: поддерживать 100% MC/DC дорого и трудоёмко, и для типового приложения это, скорее всего, не окупается. Оправдано это для инфраструктурной библиотеки, развёрнутой на миллиардах устройств, где цена ошибки огромна, а сама природа продукта — «помнить» прошлое.

Поэтому правильное чтение этого документа — брать принципы, а не цифру. Цель не в том, чтобы завтра достичь 100% покрытия, а в том, чтобы понимать, какие уровни защиты существуют, и осознанно выбирать, до какого доходить, исходя из цены ошибки в своём продукте.

02 · архитектурный принцип

Не один большой набор тестов, а четыре независимых

Ключевое решение SQLite — не наращивать один гигантский набор тестов, а держать четыре независимо разработанных и сопровождаемых стенда. Логика проста: у любого набора тестов есть слепые зоны, но у четырёх разных стендов они разные. Разнообразие углов проверки ценнее, чем суммарный объём. Это инженерная версия «правила Линуса»: при достаточном числе независимых наблюдателей любой баг становится мелким.

A
TCL Tests
public domain

Оригинальный набор, живёт в том же дереве исходников, что и ядро. Основной инструмент разработки: именно его гоняют чаще всего. Перед каждым коммитом разработчик прогоняет подмножество veryquick — оно ловит большинство ошибок за минуты, а не часы.

51 445 уникальных кейсов1390 файлов · 23.2 МБveryquick ≈ 304.7 тыс. кейсов
B
TH3
проприетарный · авиационный класс

Набор на C, дающий 100% покрытие ветвей и 100% MC/DC ядра. Главная особенность: TH3 тестирует скомпилированный объектный код через публичные интерфейсы, а не исходники — то есть проверяет в том числе, что баг не внёс сам компилятор. Девиз — «тестируй то, на чём летаешь, и летай на том, что тестируешь». Работает на embedded-платформах без поддержки рабочих станций.

50 362 кейса → 2.4 млн экземпляровsoak перед релизом ≈ 248.5 млн тестов
C
SQL Logic Test
дифференциальный оракул

Прогоняет огромные объёмы SQL одновременно против SQLite и нескольких других СУБД — PostgreSQL, MySQL, SQL Server, Oracle 10g — и сверяет, что все вернули одинаковый ответ. Это решает фундаментальную проблему: «а как вообще узнать правильный ответ на миллион запросов, не прописывая его вручную».

7.2 млн запросов1.12 ГБ тестовых данных
D
dbsqlfuzz
проприетарный фаззер

Фаззер на базе libFuzzer с собственным структурно-осведомлённым мутатором. В отличие от других фаззеров, мутирует и SQL, и сам файл базы данных одновременно — и за счёт этого добирается до состояний ошибок, недостижимых иначе. Держится запущенным на трунке постоянно.

336 seed-файлов~16 ядер 24/7~500 млн кейсов в день
03 · ядро документа

Восемь уровней тестирования

Это не лестница, где, поднявшись выше, бросаешь нижние ступени. Это эшелонированная оборона — слои, работающие одновременно. Каждый ловит свой класс дефектов; вместе они не оставляют зазоров.

ЧТО ПРИЛЕТАЕТ В ПРОДУКТбитый вводOOMотказ дискапотеря питанияфаззингрегресскорректностьустойчивостьдисциплина01Функциональная корректностьрегрессии логики02Дифференциальный оракулошибки семантики03Граничные значенияoff-by-one04Аномальные сбоиOOM · I/O · крах05Fuzz-тестированиевраждебный ввод06Повреждённые данныебитый файл07Динамический анализинварианты · UB08Регрессионные тестывозврат старых баговДАННЫЕ ПОЛЬЗОВАТЕЛЯто, что нельзя потерять
01корректность

Функциональная корректность

Базовый слой: даёт ли движок правильный ответ на правильный вход.

Принцип
Миллионы параметризованных кейсов покрывают штатные сценарии. Дёшево, быстро, обязательно.
Как у SQLite
Подмножество veryquick (~304 тыс. кейсов) гоняется перед каждым коммитом — достаточно, чтобы поймать большинство ошибок за минуты.
Что ловит
Регрессии в основной логике, очевидные поломки, ошибки в новой фиче на штатных данных.
02корректность

Дифференциальный оракул

Как проверить правильность ответа, когда вручную прописать его невозможно.

Принцип
Один и тот же вход подаётся в несколько независимых реализаций; расхождение = баг хотя бы в одной.
Как у SQLite
7.2 млн SQL-запросов прогоняются против SQLite и четырёх других СУБД; ответы сверяются автоматически.
Что ловит
Тонкие ошибки семантики, где код «работает», но считает не то — и где нет заранее известного эталона.
03корректность

Граничные значения

Большинство багов живёт на краях диапазонов, а не в их середине.

Принцип
Целенаправленно толкать систему ровно к её пределам — и на шаг за них.
Как у SQLite
Тесты на макс. число столбцов, длину SQL, переполнение integer. Макрос testcase() гарантирует, что покрыты обе стороны каждой границы.
Что ловит
Off-by-one, переполнения, некорректную обработку «ровно на пределе» и «чуть за пределом».
04устойчивость к сбоям

Аномальное тестирование

Поведение, когда отказывает окружение: память, диск, питание.

Принцип
Не «случается ли сбой когда-нибудь», а детерминированно перебрать каждую точку, где сбой возможен.
Как у SQLite
Подменный malloc() падает на N-й аллокации; подменный VFS симулирует отказ диска и потерю питания, переупорядочивая и портя несинхронизированные записи. В цикле точка отказа сдвигается, пока операция не пройдёт целиком. После каждого прогона — integrity_check.
Что ловит
Утечки и порчу данных при OOM, ошибках ввода-вывода и крахах. Отдельно — составные сбои: ошибка ввода-вывода во время восстановления после прошлого краха.
05устойчивость к атаке

Fuzz-тестирование

Устойчивость к враждебному, бессмысленному и злонамеренному вводу.

Принцип
Профиль-направленный фаззер инструментирует код, генерирует мутации входа и сохраняет те, что открыли новый путь исполнения, — затем мутирует их дальше. Так находятся состояния, которые не предусмотрел ни один разработчик.
Как у SQLite
Путь от AFL (2015) к libFuzzer. dbsqlfuzz мутирует SQL и файл БД сразу; jfuzz портит JSONB-блобы; сторонние фаззеры (напр. М. Риггера) находят не падения, а неверные ответы. «Интересные» исторические кейсы прогоняются на каждом make test через fuzzcheck.
Что ловит
Падения, переполнения буфера, неопределённое поведение и редкие неверные ответы на корректном, но безумном вводе.
06устойчивость к атаке

Повреждённые данные

Что делает движок, когда ему подсовывают битый файл хранилища.

Принцип
Сборка должна корректно отклонять повреждённый вход, а не падать на нём.
Как у SQLite
Берётся корректный файл БД, байты портятся в обход движка, затем файл читается. Проверяется, что ошибка формата ловится и сообщается кодом SQLITE_CORRUPT без переполнений буфера и разыменования NULL.
Что ловит
Уязвимости при чтении недоверенных или испорченных данных — классический вектор атаки.
07внутренние инварианты

Динамический анализ

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

Принцип
Код сам непрерывно проверяет свои предположения, пока выполняется, — а не только постфактум по результату.
Как у SQLite
6754 assert() на пред/постусловия и инварианты циклов (в debug-сборке; в проде выключены — с ними движок втрое медленнее). Прогоны под Valgrind. Проверки неопределённого поведения: -fsanitize=undefined, -ftrapv, /RTC1, на 32/64-бит и big/little-endian. Авто-детект утечек памяти, дескрипторов и мьютексов на каждом прогоне.
Что ловит
Нарушенные инварианты, выходы за границы массива, чтение неинициализированной памяти, утечки, неопределённое поведение C.
08необратимость прогресса

Регрессионное тестирование

Однажды исправленный баг не должен вернуться никогда.

Принцип
Баг не считается исправленным, пока не написан тест, который его воспроизводил.
Как у SQLite
Каждый отчёт о баге превращается в постоянный кейс в TCL или TH3. За годы — тысячи таких тестов.
Что ловит
Повторное появление уже известных ошибок. Особенно критично для БД, которая «помнит» прошлые ошибки в данных.

// слои 01–03 → корректность · 04–06 → устойчивость · 07–08 → внутренняя дисциплина

04 · строгость измерения

Что на самом деле значит «100% покрытие»

Когда говорят «у нас XX% покрытия», почти всегда имеют в виду покрытие операторов (statement coverage) — какой процент строк хоть раз выполнился. SQLite держит куда более строгую метрику — покрытие ветвей (branch coverage). Разница принципиальна. Возьмём строку:

if( a>b && c!=25 ){ d++; }

Один проход, где условие истинно, даёт 100% покрытия операторов — строка «протестирована». Но чтобы покрыть все ветви, нужно минимум три случая:

  • a <= b
  • a > b && c == 25
  • a > b && c != 25

100% покрытия ветвей всегда влечёт 100% операторов, но не наоборот. Поверх этого SQLite добивается ещё и MC/DC (Modified Condition/Decision Coverage) — стандарта, применяемого в авиационном ПО: каждое условие в решении должно быть показано как независимо влияющее на результат.

Три приёма, которые делают это работающим

Защитный код не удаляют ради метрики. Условия, которые в проде всегда истинны/ложны, помечают макросами ALWAYS()/NEVER(). Они исключаются из подсчёта покрытия, но в debug-сборке падают с ошибкой, если предположение нарушено.

gcov как мета-тест. Прогон с замером покрытия тестирует не сам SQLite, а качество набора тестов — доказывает, что тесты доходят до каждой ветви. Реальная проверка движка идёт отдельной сборкой, в боевой конфигурации компилятора.

Мутационное тестирование. Каждую инструкцию ветвления по очереди меняют на «всегда» или «никогда» и проверяют, что тест это заметил. Если не заметил — либо ветка лишняя (можно удалить), либо тест слаб. Плюс весь набор гоняется с включёнными и выключенными оптимизациями: ответ обязан совпасть.

Честное напряжение, о котором стоит знать

Фаззинг и 100% MC/DC тянут в разные стороны. MC/DC не любит защитный код с недостижимыми ветвями — а именно такой код спасает от фаззера. Код, идеально проходящий fuzzing, обычно имеет заметно меньше 100% MC/DC. SQLite держит оба, но большинство процессорного времени сегодня уходит на fuzzing. Идеального «и то, и другое разом» не существует — это всегда осознанный компромисс.

05 · релизный шлюз

Последний рубеж — это человек со списком

Перед каждым релизом проходится чек-лист примерно из 200 пунктов, вручную. Часть отмечается за секунды, часть запускает тесты на много часов. Подход вдохновлён книгой «Чек-лист. Как избежать глупых ошибок, ведущих к фатальным последствиям» Атула Гаванде.

Принципиально, что список сознательно не автоматизирован полностью. На верхнем уровне нужен человек, который смотрит на вывод тестов и спрашивает: «А это правда корректно?». Бывает, что тест формально проходит, но человек замечает рядом проблему. Список постоянно эволюционирует — каждая новая обнаруженная проблема добавляет в него пункт.

Контринтуитивный вывод от самих авторов

Статический анализ им почти не помог. По их собственному признанию, больше багов было внесено при попытках убрать предупреждения анализаторов, чем найдено этими анализаторами. Вывод не «статический анализ бесполезен везде», а более тонкий: набор тестов был настолько силён, что отдача от статического анализа на его фоне оказалась мала. Сначала — тесты.

06 · как применить у себя

Лестница зрелости, а не чек-лист на завтра

100% MC/DC копировать слепо не нужно — это для инфраструктуры с ценой ошибки в миллиарды устройств. Переносится не цифра, а принципы. Вот порядок, в котором они окупаются.

01

Каждый баг → постоянный регрессионный тест

Минимальная практика, которая окупается сразу же и не даёт исправленным ошибкам вернуться. Если внедрять что-то одно — это.

02

Тестировать сбои, а не только happy path

Что происходит при отказе сети, пустом ответе, таймауте, битых данных. Большинство продакшен-инцидентов живёт именно здесь.

03

Разнообразие проверок важнее их объёма

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

04

Дифференциальный оракул, когда есть с чем сверять

Старая реализация, эталонный сервис, вторая независимая ветка логики — если ответ можно сверить автоматически, не прописывайте эталоны руками.

05

Branch coverage честнее statement coverage

Как метрика — измеряйте ветви, а не строки. «90% строк» и «90% ветвей» — это две очень разные степени уверенности.

06

Человек в релизном контуре

Чек-лист релиза и финальный взгляд живого человека, спрашивающего «это правда корректно?», ловят то, что зелёный CI пропускает.

Если запомнить одну мысль

Уровень усилий на тестирование должен соответствовать цене ошибки в конкретном продукте. SQLite — крайняя точка спектра, где эта цена максимальна. Ваш продукт почти наверняка не требует 100% MC/DC — но эта карта показывает, какие рубежи обороны вообще существуют, чтобы вы осознанно выбирали, до какого дойти, а не упирались в один уровень, не зная об остальных.

P.S. · не только теория

Этот сайт построен по этой карте

Мы не просто пересказали SQLite — те же 8 рубежей воплощены в коде самого almazrobots.ru, на уровне адекватности для веб-приложения. 607+ тестов: юнит на всю чистую логику (auth-крипта, деньги, агрегации, csv), fault-injection на файловое хранилище, property-based фаззинг недоверенных парсеров, mutation-testing критичных модулей (score ~87% — тесты «с зубами»), корпус регресс-фикстур и релиз-чек-лист. Тесты — жёсткий гейт деплоя: красный тест не уезжает в прод.

Что переносится в любой проект

Цена ошибки разная, а дисциплина одна: баг не закрыт, пока его вход не лёг в корпус; тестируй отказы, а не только happy path; разнообразие проверок важнее их количества; человек на вершине релиза.

Цена ошибки у вас своя —
но карта обороны одна

SQLite — крайняя точка спектра. Вам столько не нужно — но знать, какие рубежи существуют, полезно всем. Рядом — такие же разборы «зачем и как» про работу с ИИ-агентами: каталог практик agentic coding.

Эталон тестирования продукта

Универсальный концептуальный разбор. Не привязан к конкретному проекту — ориентир культуры тестирования.

Источник фактов и метрик: официальная документация SQLite — sqlite.org/testing.html и sqlite.org/th3.html · данные приведены по состоянию на апрель 2026.

Алмаз Салимзянов · AI & Automation · almazrobots