Таблица случаев
Четыре случая — это четыре куска по пять строк, и различаются в них два числа. Копировать такое вручную неприятно и опасно: поправил один кусок, забыл остальные. В Go для этого есть привычная форма — таблица случаев, по-английски table-driven test. Так тесты пишут в самом Go и почти во всех проектах на нём.
Четыре случая в лоб
Ситуация. К price нужны четыре проверки: обычный заказ, ровно порог, ноль и крупный заказ.
func TestPrice(t *testing.T) {
got := price(3)
if got != 13500 {
t.Errorf("price(3) = %d, хотели 13500", got)
}
got = price(12)
if got != 48600 {
t.Errorf("price(12) = %d, хотели 48600", got)
}
got = price(0)
if got != 0 {
t.Errorf("price(0) = %d, хотели 0", got)
}
got = price(20)
if got != 81000 {
t.Errorf("price(20) = %d, хотели 81000", got)
}
}Работает. Но пятый случай — это ещё четыре строки копипасты, и в одной из них обязательно останется чужое число из предыдущего куска.
Та же проверка таблицей
Ниже — готовая форма. Сейчас её читают: номера слева нужны разбору и вопросу q4, в файле их не будет. Набирать форму — в блоке «Попробуйте сейчас» ниже, уже без номеров; свои случаи подставите туда же. Устройство разберём в блоках 4 и 5, когда появятся срезы и структуры. Сейчас достаточно понимать, что делает каждая часть — это видно по именам.
1 package main
2
3 import "testing"
4
5 func TestPrice(t *testing.T) {
6 cases := []struct {
7 name string
8 qty int
9 want int
10 }{
11 {"обычный заказ", 3, 13500},
12 {"ровно порог", 12, 48600},
13 {"ноль", 0, 0},
14 {"крупный", 20, 81000},
15 }
16 for _, c := range cases {
17 got := price(c.qty)
18 if got != c.want {
19 t.Errorf("%s: price(%d) = %d, хотели %d", c.name, c.qty, got, c.want)
20 }
21 }
22 }Что увидите.
stagiaire@lab:~/gocourse/day14/scratch$ go test .
ok day14/scratch 0.005sКак это читать. По частям.
| Строки | Как читать |
|---|---|
| 6–10 | «случаи — это список записей, у каждой записи три поля: name — название словами, qty — что подадим на вход, want — что ожидаем» |
| 11–14 | сами случаи, по одному в строке: название, вход, ожидаемое. Порядок значений — как в описании полей выше |
| 16 | «для каждого случая c из списка cases» |
| 17 | вызвали функцию входом из случая: c.qty — поле qty текущего случая |
| 18–20 | то же сравнение, что было, только ожидаемое берётся из случая: c.want |
Имена полей выбираете вы: name, qty, want — самые ходовые. Точка между c и именем поля читается как «поле такое-то у случая c». for _, c := range cases — это «пройти список по очереди». Это новая форма for: в блоке 2 цикл писали со счётчиком и условием (for i := 0; i < n; i++), а range идёт по готовому списку сам, без счётчика и без условия конца — устройство разберём в блоке 4. Подчёркивание на месте первого значения знакомо по дню 12: «эта часть не нужна».
Что изменилось по сути: добавить случай — это добавить одну строку в скобках между 14 и 15. Ни if, ни t.Errorf больше не копируются.
Попробуйте сейчас: та же проверка таблицей.
Цель:
scratch/price_test.go— одна тестовая функция с таблицей из четырёх случаев,go test .зелёный.1. Замените содержимое файла на ту же форму — здесь она без номеров строк, набирать нужно её:
✎ Наберите в файлscratch/price_test.gopackage main import "testing" func TestPrice(t *testing.T) { cases := []struct { name string qty int want int }{ {"обычный заказ", 3, 13500}, {"ровно порог", 12, 48600}, {"ноль", 0, 0}, {"крупный", 20, 81000}, } for _, c := range cases { got := price(c.qty) if got != c.want { t.Errorf("%s: price(%d) = %d, хотели %d", c.name, c.qty, got, c.want) } } }2. Запустите и отформатируйте:
▶ Выполнитеcd ~/gocourse/day14/scratch go test . gofmt -l .Готово, когда:
go test .печатаетok,gofmt -l .ничего не печатает, а в таблице ровно четыре строки со случаями — как в форме выше. Пятый случай допишем ниже, отдельным опытом.
Что печатает упавший табличный тест
Здесь у формы есть особенность, из-за которой имя случая — не украшение. Испортим ожидаемое в двух случаях: у «ровно порог» напишем 48000, у «крупный» — 80000.
Попробуйте сейчас: номер строки в упавшем табличном тесте.
Цель: предсказать, какой номер строки напечатает упавший табличный тест, и сдать ответ.
1. Не запуская: посмотрите на форму с номерами строк в разделе «Та же проверка таблицей» — в ней четыре случая, ровно столько же в вашем файле. Ожидаемое неверно у двух случаев. Какой номер строки будет в обеих строках вывода — после
price_test.go:?От числа случаев этот номер не зависит:
t.Errorfв таблице один. А вот от числа строк выше него — зависит, поэтому случаев в файле должно быть четыре, как в форме: допишете пятый — номер сдвинется на единицу.Вывод этого прогона напечатан сразу под блоком — смотрите туда после того, как отправите ответ.
2. Сдать число:
▶ Выполните · выделенное замените своимcourse answer day14.q4 ЧИСЛО3. Проверьте запуском: испортите ожидаемое у двух случаев — у «ровно порог» напишите 48000, у «крупный» 80000, — запустите
go test .и верните числа обратно.▶ Выполнитеcd ~/gocourse/day14/scratch go test .Готово, когда: пункт
q4вcourse check 14зелёный, а в вашем файле снова верные ожидаемые числа.
Что увидите.
stagiaire@lab:~/gocourse/day14/scratch$ go test .
--- FAIL: TestPrice (0.00s)
price_test.go:19: ровно порог: price(12) = 48600, хотели 48000
price_test.go:19: крупный: price(20) = 81000, хотели 80000
FAIL
FAIL day14/scratch 0.005s
FAILСтрок две — по одной на несошедшийся случай, и номер строки в обеих одинаковый. Это не опечатка: t.Errorf в таблице один, он стоит в строке 19, и именно её номер печатается всегда. Номер строки больше не показывает, какой случай сломался.
Показывает это c.name — первое, что стоит в сообщении. Поэтому в табличном тесте название случая в t.Errorf обязательно, а название «случай 1» бесполезно: пишите словами, что именно проверяется.
Ещё одно следствие: t.Fatalf в таблице останавливает весь цикл, а не текущий случай. Из четырёх случаев вы увидите первый несошедшийся и ничего про остальные. В таблице почти всегда нужен t.Errorf.
Попробуйте сейчас: пятый случай — одна строка.
Цель: увидеть своими глазами, во что обходится новый случай в таблице, и вернуть файл к четырём случаям.
1. Допишите пятый случай — 11 штук — одной строкой внутри фигурных скобок со случаями, после
{"крупный", 20, 81000},. Ожидаемое посчитайте сами: 11 × 4500, скидки ещё нет.▶ Выполнитеcd ~/gocourse/day14/scratch go test . gofmt -l .2. Сравните с разделом «Четыре случая в лоб»: там пятый случай — это ещё четыре строки копипасты, здесь одна.
3. Уберите эту строку обратно. Это обязательно: на следующей странице мы дописываем в тот же файл вторую тестовую функцию и считаем номера строк от таблицы из четырёх случаев — с пятым все номера сдвинутся на единицу.
▶ Выполнитеgo test .Готово, когда:
go test .печатаетok, а в таблице снова четыре строки со случаями — пятый вы добавили, посмотрели и убрали.
Попробуйте сейчас: задание packs — таблица случаев.
Цель: файл
packs_test.goв каталоге~/gocourse/day14/packsловит обе подмены реализации.1. Прочитайте условие — размер коробки у вас свой:
▶ Выполнитеcd ~/gocourse/day14/packs cat TASK.txt go run .2. Наберите
packs_test.goпо форме выше: одна тестовая функция, таблица не меньше чем на четыре случая. Обязательно возьмите партию, которая делится на размер коробки ровно, и партию 0 шт.; остальные случаи — на ваш выбор. Ожидаемые числа считайте по условию.3. Запустите:
▶ Выполнитеgo test . go test -v . gofmt -l .4. Проверьте силу таблицы приёмом со страницы 3: временно замените тело функции
packsвpacks.goнаreturn qty/РАЗМЕР + 1, гдеРАЗМЕР— ваше число штук в коробке, и запуститеgo test .. Тест обязан упасть. После опыта верните файл как был.5. Задание готово — закоммитьте:
▶ Выполнитеcd ~/gocourse/day14 git add . git commit -m "таблица случаев для packs"Готово, когда:
go test .печатаетokна верной функции иFAILна подменённой — пунктыtableиtablezero. Проверка смотрит и на форму теста: одна тестовая функция, таблица и цикл по ней, не меньше четырёх строк-случаев — это пунктpacktable.
Что может пойти не так
| Что видите | Что это значит | Что делать |
|---|---|---|
price_test.go:11:42: missing ',' before newline in composite literal |
в строке случая забыта запятая в конце | после каждой строки со случаем — запятая, включая последнюю |
too few values in struct literal of type struct{name string; qty int; want int} |
в случае меньше значений, чем полей в описании | столько же значений, сколько полей, и в том же порядке |
две строки сразу: cannot use 3 (untyped int constant) as string value in struct literal и cannot use "обычный заказ" (untyped string constant) as int value in struct literal |
значения перепутаны местами: число встало на место названия, а название — на место числа | порядок значений — как порядок полей: name, qty, want. Go находит обе половины перестановки и печатает про каждую |
undefined: c |
тело цикла написано вне for |
проверка и t.Errorf — внутри фигурных скобок цикла |
| в выводе одинаковый номер строки и непонятно, какой случай упал | в сообщении нет c.name |
первым в t.Errorf — название случая |
| упал только один случай, хотя неверных два | в таблице t.Fatalf |
в таблице — t.Errorf |
declared and not used: c |
случай получен, а поля не используются | внутри цикла вызывать функцию от c.qty |
Дальше: тест, который ловит найденный дефект, — course next