Сначала падающий тест, потом починка
В дне 09 была методика поиска дефекта: воспроизвести, сузить, гипотеза, проверка, починка, повторный прогон. Первый и последний шаги там делались руками — запустить программу на том вводе, где неверно, и потом ещё раз на всех остальных. Сегодня оба шага можно записать один раз и оставить в проекте навсегда.
Тест, который написан до починки и на ней падает, называют репродьюсером: он воспроизводит дефект.
Зачем писать тест раньше починки
Ситуация. Кладовщик жалуется: «на заказе ровно в 12 штук сумма больше, чем в накладной». Дефект уже понятен: в price.go вместо qty >= 12 написано qty > 12.
Рука тянется исправить один знак и закрыть вопрос. Что при этом теряется:
| Без репродьюсера | С репродьюсером |
|---|---|
| «кажется, починил» — проверено глазами один раз | тест сначала красный, после правки зелёный: разница видна |
| не доказано, что вы поняли именно этот дефект | падающий тест — доказательство: вы назвали вход и ответ заранее |
через месяц кто-то вернёт > при правке соседней строки |
тест поймает это в тот же день |
| случай остаётся в голове | случай остаётся в проекте |
Последняя строка — главная. Возврат починенного дефекта называют регрессом, и тест-репродьюсер — единственная дешёвая защита от него.
Как это выглядит
Шаг 1. Воспроизвести. Ставим в price.go дефект — qty > 12 вместо qty >= 12, — и смотрим, что функция считает на 12 штуках:
stagiaire@lab:~/gocourse/day14/scratch$ go run .
13500
5400012 × 4500 = 54000 — скидки нет, хотя по условию она с 12 штук уже есть. Ожидаемое: 48600.
Шаг 2. Записать это тестом. Отдельная тестовая функция с именем, из которого видно, про что она. Дописываем её в конец scratch/price_test.go — там уже лежит таблица со страницы 5, она занимает 22 строки, поэтому новая функция начинается с 24-й:
24 func TestPriceOnThreshold(t *testing.T) {
25 got := price(12)
26 want := 48600
27 if got != want {
28 t.Errorf("price(12) = %d, хотели %d", got, want)
29 }
30 }Запускаем только её — по имени, как на странице 2:
stagiaire@lab:~/gocourse/day14/scratch$ go test -run TestPriceOnThreshold .
--- FAIL: TestPriceOnThreshold (0.00s)
price_test.go:28: price(12) = 54000, хотели 48600
FAIL
FAIL day14/scratch 0.005s
FAILНомер строки — 28, потому что именно там стоит t.Errorf новой функции; у вас он будет свой, смотря сколько строк в файле выше.
Красный тест — это успех шага. Он доказывает две вещи: дефект есть и тест его видит.
Шаг 3. Починить. Одна правка: qty > 12 обратно в qty >= 12.
Шаг 4. Повторный прогон. Не только новый тест, а все:
stagiaire@lab:~/gocourse/day14/scratch$ go test -v .
=== RUN TestPrice
--- PASS: TestPrice (0.00s)
=== RUN TestPriceOnThreshold
--- PASS: TestPriceOnThreshold (0.00s)
PASS
ok day14/scratch 0.008sВот здесь тесты окупаются в первый раз: шестой шаг методики дня 09 — «не сломалось ли остальное» — обошёлся в одну команду вместо десяти запусков руками.
Если бы репродьюсер оказался зелёным ещё до починки, это значило бы, что дефект в другом месте или на другом входе, — и чинить было бы рано.
Попробуйте сейчас: репродьюсер в черновике.
Цель: увидеть весь порядок на своей программе — красный тест, правка, зелёный тест.
1. Поставьте в
scratch/price.goдефект:qty > 12вместоqty >= 12. Допишите в конецscratch/price_test.goтестовую функциюTestPriceOnThreshold— ту же, что в разделе выше, только без номеров строк:✎ Наберите в файлscratch/price_test.gofunc TestPriceOnThreshold(t *testing.T) { got := price(12) want := 48600 if got != want { t.Errorf("price(12) = %d, хотели %d", got, want) } }Файл целиком не переписывайте: таблица со страницы 5 остаётся выше, новая функция идёт после неё. Запустите только её:
▶ Выполнитеcd ~/gocourse/day14/scratch go test -run TestPriceOnThreshold .2. Убедитесь, что она упала, и почините
price.go.3. Прогоните все тесты каталога:
▶ Выполнитеgo test -v .Готово, когда: до починки
go test -run TestPriceOnThreshold .печаталFAIL, после починкиgo test -v .печатаетPASSу каждого теста иokв конце.
Имя теста — тоже сообщение
Тестовых функций в файле становится несколько, и имя каждой должно говорить, что именно она защищает. Сравните:
| Имя | Что о нём думает читатель |
|---|---|
TestPrice1, TestPrice2 |
порядок написания; про что они — неизвестно |
TestPriceOnThreshold |
про границу порога |
TestPriceZero |
про нулевой заказ |
TestPriceBulk |
про заказ со скидкой |
Имя начинается с Test, дальше — заглавная буква, и дальше по-английски без пробелов. Это не прихоть: по этому имени вы будете выбирать тест в -run, и по нему же о падении узнает тот, кто вашего кода не видел.
Если репродьюсер не падает
Красный тест на шаге 2 — не формальность, а единственное доказательство, что вы поняли дефект. Поэтому зелёный репродьюсер — повод остановиться и разобраться, а не идти чинить.
Ситуация. Дефект тот же — qty > 12 вместо qty >= 12, — а случай для теста взят рядом с границей, не на ней: 13 штук вместо 12. 13 × 4500 = 58500, минус десятая часть 5850 — 52650.
func TestPriceOnThreshold(t *testing.T) {
got := price(13)
want := 52650
if got != want {
t.Errorf("price(13) = %d, хотели %d", got, want)
}
}stagiaire@lab:~/gocourse/day14/scratch$ go test -v -run TestPriceOnThreshold .
=== RUN TestPriceOnThreshold
--- PASS: TestPriceOnThreshold (0.00s)
PASS
ok day14/scratch 0.005sPASS на сломанной функции. И это правда: на 13 штуках обе версии считают одинаково — скидка есть и там, и там. Дефект живёт ровно в одной точке, а тест смотрит мимо неё на одну штуку.
Вторая причина зелёного теста обиднее: тест вообще не запускался. Опечатка в имени после -run даёт почти такой же вывод:
stagiaire@lab:~/gocourse/day14/scratch$ go test -run TestPriceOnTreshold .
ok day14/scratch 0.006s [no tests to run]Отличие — [no tests to run] в конце строки. Поэтому репродьюсер запускают с -v: === RUN с именем теста в выводе значит, что он действительно выполнился.
| Что видите | Что это значит | Что делать |
|---|---|---|
--- PASS на ещё не починенной функции |
случай взят не там, где дефект | пересчитать по условию и взять ровно ту точку, где числа расходятся |
ok … [no tests to run] |
тест не запускался: опечатка в имени после -run |
сверить имя с тем, что написано в файле, или запустить без -run |
--- FAIL, но числа в сообщении оба неверные |
ожидаемое посчитано неправильно | считать по условию на бумаге, а не по выводу программы |
| тест падает и после починки, и до неё | тест проверяет не ту функцию либо ожидаемое неверно | запустить go test -v . и прочитать сообщение целиком |
Попробуйте сейчас: задание deliver — починка по упавшему тесту.
Цель: в
~/gocourse/day14/deliverесть тест, который падал до починки, а сама программа считает верно.1. Прочитайте условие и запустите программу на том количестве, с которого начинается бесплатная доставка (число — в
TASK.txt):▶ Выполнитеcd ~/gocourse/day14/deliver cat TASK.txt go run .Программа ждёт число со ввода: наберите его и нажмите Enter.
2. Посчитайте по условию, сколько должно получиться, и сравните. Нашли количество, на котором не сходится, — напишите
total_test.goс тестом на этот случай. Функция называетсяtotal,main.goона не трогает — тест проверяет функцию, а не вывод программы.3. Запустите тест и убедитесь, что он падает:
▶ Выполнитеgo test .4. Закоммитьте тест, пока он падает, — отдельным коммитом:
▶ Выполнитеcd ~/gocourse/day14 git add . git commit -m "тест на границу бесплатной доставки, пока падает" cd ~/gocourse/day14/deliver5. Почините
total.go— правка одна-две строки, больше двух проверка не примет. Запустите тест снова и прогоните программу:▶ Выполнитеgo test . printf '1\n' | go run .6. Посмотрите на свою правку и закоммитьте починку:
▶ Выполнитеdiff ~/.course/orig/day14/deliver/total.go total.go cd ~/gocourse/day14 git add . git commit -m "починка границы в total" git log --onelineДва коммита вместо одного — не для красоты: между ними в истории видно, что тест сначала падал. Это лучшее доказательство, что починка настоящая.
Готово, когда:
go test .зелёный, программа на обеих сторонах границы печатает верные суммы, аdiffпоказывает одну-две изменённые строки — пунктыfix,fixtest,reprodиfixsmall.
Чего репродьюсер не делает
Тест не ищет дефект за вас. Он фиксирует случай и ответ, а первые четыре шага методики дня 09 — воспроизвести, сузить, гипотеза, проверка — остаются вашей работой: отладочная печать, dlv, чтение кода.
И ещё: репродьюсер не заменяет остальные случаи. После починки в тесте должны остаться и обычный случай, и ноль, и вторая сторона границы. Иначе следующая правка починит границу и сломает всё остальное — а тест про это промолчит.
Ожидаемое для нуля берите из условия, а не из общих соображений: в deliver заказ 0 шт. считается по той же формуле, что остальные, — 0 коп. за товар плюс доставка. «Ноль штук — ноль к оплате» здесь было бы другим правилом, и в TASK.txt его нет.
Что может пойти не так
| Что видите | Что это значит | Что делать |
|---|---|---|
| репродьюсер зелёный до починки | взят не тот вход или не то ожидаемое | пересчитать по условию; дефект часто ровно на границе, а не рядом |
| после починки тест всё ещё красный, числа странные | ожидаемое в тесте посчитано неверно | считать по условию, а не по выводу программы |
| после починки красным стал другой тест | правка сломала соседний случай | вернуть файл из ~/.course/orig/day14 и чинить точечно |
undefined: total в тесте |
тест лежит не в том каталоге или в нём другой package |
файл _test.go — рядом с total.go, package main |
| правка вышла на 5 строк | переписали функцию вместо починки | cp ~/.course/orig/day14/deliver/total.go total.go и поменять один знак |
go run . ничего не печатает и ждёт |
программа читает число со ввода | набрать число и нажать Enter или подать ввод конвейером: printf '1\n' | go run . |
Дальше: ритуал перед коммитом — course next