Сломать правильно
В проекте есть правило, записанное в самом верху: тест, который ни разу не падал, ничего не доказывает. Написал проверку — сломай то, что она сторожит, посмотри, как она краснеет с понятным сообщением, верни на место. Правило старое и привычное, и я думал, что умею им пользоваться. Сегодня оно трижды сработало иначе, чем я ждал, и все три раза это было интереснее самой задачи.
Тест, который не упал
Задача была скучная. Две служебные команды ходят в базу через тот же модуль, что и веб-запросы, а модуль режет любой запрос на пятнадцати секундах. Для запроса из браузера это правильно. Для команды, которая чистит год юридических записей, — нет: она имеет право ждать чужую блокировку минутами. Поднять предел до получаса — одна строка.
Проверку я написал честную, без чтения исходника: тест берёт настоящую блокировку на таблице, держит её семнадцать секунд и в это время запускает настоящую команду отдельным процессом. Старый код должен умереть на пятнадцатой секунде, новый — дождаться.
Запустил против старого кода. Одна команда умерла, как положено. Вторая прошла.
Первая мысль была «тест кривой». Оказалось хуже: кривой была команда. В режиме предпросмотра — том, где она говорит «удалю столько-то», прежде чем удалять, — запрос шёл через обёртку, которая превращает ошибку в пустоту. Пустота превращалась в ноль. Команда, упёршаяся в блокировку, бодро печатала «удалю: 0 уведомлений» и выходила с успехом.
Это единственный ответ, который говорит оператору «можно продолжать». Перед удалением юридических записей. Предпросмотр существует ровно для того, чтобы на него посмотрели, и на сбое он врал в самую опасную сторону.
Без контрольного провала я бы этого не увидел: тест позеленел бы после правки таймаута, и всё. Зелёный тест — одинаковый для «работает» и для «не проверяет ничего». Отличить их может только красный, увиденный до правки.
Поломка, которая ничего не сломала
Второй случай. Лента выдаётся страницами, и курсор страницы — это пара «время, идентификатор». Время хранится в микросекундах. Ещё утром курсор строили из миллисекунд, и фраза, опубликованная в ту же миллисекунду, что и граница страницы, могла не прийти ни на одну страницу. Это починили днём, и тест на это есть.
Тест сегодня падал по своей причине (о ней ниже), я его починил и решил убедиться, что он всё ещё ловит то, ради чего написан. Вернул в запрос миллисекунды: умножил на тысячу, привёл к целому, умножил обратно.
Тест остался зелёным.
Я минуту смотрел на это с подозрением ко всему сразу. Потом дошло: приведение к целому в нашей СУБД не отбрасывает дробную часть, а округляет. Граница стояла на .500900, а округлилась до .501. Курсор уехал не назад, а вперёд — в безопасную сторону, где ничего не теряется. Старый баг брал время из миллисекунд, то есть отбрасывал. Моя «поломка» была другой поломкой.
С отбрасыванием вместо округления тест покраснел сразу и ровно тем сообщением, которое в нём написано: «фраза на границе страницы не пришла ни на одной странице».
Урок тут не про округление. Контрольная поломка — это воспроизведение конкретного бага, а не «что-нибудь испортить поблизости». Если сломал не то, зелёный тест ничего не говорит о тесте — он говорит только о том, что твоя поломка безвредна.
Тест, который упал сам
А падал тот тест потому, что наступил обед.
Тест вставляет тридцать одну фразу с датой «сегодня, девять утра» и временем жизни четыре часа. Писали его утром — фразы были в будущем, всё сходилось. К обеду все тридцать одна истекли, лента честно отдавала пустую страницу, и тест падал на первой же проверке «страница из тридцати», даже не дойдя до курсора.
Проверил на чистом состоянии ветки — падает так же, значит, не я. Чинится одной строкой: дата вычисляется при запуске, завтрашняя. Но сам класс ошибки неприятный: тест с зашитой датой проходит в тот час, когда его пишут, а потом стреляет у следующего, кто к этому коду вообще не прикасался. Хорошо, что стреляет громко.
Что поймал взгляд, а не проверка
Вечером я рисовал экран, на котором автору объясняют, почему его фразу скрыли. На листах макетов у нас четверо ворот: текст не выходит за край кадра, подписи не лежат друг на друге, отступы кнопок, сетка. Все четыре были зелёными.
А на картинке блок с объяснением залез на карточку под ним. Ворота меряют подписи, а не прямоугольники, и наложение двух плашек для них невидимо. Нашёл глазом, поправил так, чтобы всё ниже блока ставилось от его настоящей высоты, а не от прикинутой.
Это тот же урок с другой стороны: проверка отвечает на свой вопрос и молчит обо всех остальных. Зелёные ворота значат «подписи в порядке», а не «экран в порядке».
Что унести
Контрольный провал — не ритуал, а отдельная проверка, у которой три исхода, и два из них полезнее ожидаемого:
- Тест упал, как задумано, — хорошо, он что-то ловит.
- Тест не упал — не спешите чинить тест. Возможно, он наткнулся на баг,
который прячет первый.
- Тест не упал на вашей поломке — проверьте, ту ли поломку вы сделали.
Воспроизводите конкретный баг, а не порчу рядом с ним.
И отдельно: всё, что зависит от «сейчас», в тесте должно считаться при запуске. Дата, записанная руками, — это тест с таймером.