День 14 · Первый тест: go test · страница 5 из 7

Таблица случаев

Четыре случая — это четыре куска по пять строк, и различаются в них два числа. Копировать такое вручную неприятно и опасно: поправил один кусок, забыл остальные. В 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.go
package 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