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

Зачем тест и как устроен первый

В днях 11–13 появились свои функции. Функцию пишут один раз, а вызывают из разных мест и правят потом много раз: поменялась цена, добавился ещё один порог, вынесли кусок в отдельную функцию. После каждой такой правки надо знать, считает ли она по-прежнему верно.

До сих пор ответ на этот вопрос вы получали руками: запустили программу, посмотрели на числа, сравнили с тем, что посчитали на бумаге. Сегодня эту работу начнёт делать программа — тест.


Ручная проверка: что с ней не так

Ситуация. Есть функция, которая считает стоимость заказа в копейках: цена 4500 коп. за штуку, от 12 штук скидка 10 процентов.

Код для чтения · разбираем, набирать не нужно
package main

func price(qty int) int {
	total := qty * 4500
	if qty >= 12 {
		total = total - total/10
	}
	return total
}

Чтобы посмотреть на неё в работе, рядом пишут main, который её зовёт:

Код для чтения · разбираем, набирать не нужно
package main

import "fmt"

func main() {
	fmt.Println(price(3))
	fmt.Println(price(12))
}

Что увидите.

Пример · только посмотреть, набирать не нужно
stagiaire@lab:~/gocourse/day14/scratch$ go run .
13500
48600

Как это читать. 3 × 4500 = 13500 — скидки нет, порог не взят. 12 × 4500 = 54000, скидка 5400, к оплате 48600. Числа сошлись с тем, что посчитано на бумаге. Функция работает.

Проверка заняла минуту, и у неё четыре беды:

Беда В чём она
результат сверяете вы программа напечатала 13500, а верно это или нет, знает только человек, который помнит условие
проверка живёт один раз завтра вы поправите функцию, и её надо будет повторить всю целиком
проверяется то, что под рукой два числа, которые вы набрали сегодня; ноль, отрицательное и ровно порог — как получится
её никто не повторит коллега, который через месяц полезет в эту функцию, не знает, что вы проверяли и с каким ответом

Тест снимает все четыре: это программа, которая вызывает вашу функцию и сама сравнивает результат с ожидаемым. Она лежит рядом с кодом, её запускают одной командой, и запускать её может кто угодно и сколько угодно раз.


Тест — это тоже программа

В Go для тестов не нужно ничего устанавливать: они встроены в язык и в команду go. Правил всего четыре, и они жёсткие — по ним go test находит тесты сам.

Правило Как это выглядит
файл называется на _test.go price_test.go, cost_test.go
файл лежит рядом с кодом, в том же каталоге и том же пакете package main первой строкой
в файле подключён пакет testing import "testing"
имя тестовой функции начинается с Test, параметр один func TestPrice(t *testing.T)

Первый тест к функции price целиком:

Код для чтения · разбираем, набирать не нужно
package main

import "testing"

func TestPrice(t *testing.T) {
	got := price(3)
	want := 13500
	if got != want {
		t.Errorf("price(3) = %d, хотели %d", got, want)
	}
}

Что увидите.

Пример · только посмотреть, набирать не нужно
stagiaire@lab:~/gocourse/day14/scratch$ go test .
ok  	day14/scratch	0.005s

Как это читать — по строкам.

Строка Что делает
package main тот же пакет, что у price.go: тест видит price напрямую, без импортов
import "testing" пакет из стандартной библиотеки, в нём тип testing.T
func TestPrice(t *testing.T) тестовая функция. Имя после Test — с заглавной буквы; параметр t — «пульт», через который тест сообщает о неудаче
got := price(3) вызвали проверяемую функцию. got — «получено»
want := 13500 ожидаемое. Это число вы считаете сами, по условию, а не берёте у программы
if got != want сравнение. Вот ради этой строки всё и написано
t.Errorf(…) пометить тест неудачным и напечатать сообщение; форматирование — как у fmt.Printf из дня 03

ok и имя пакета — весь вывод удачного прогона. day14/scratch — путь пакета: имя модуля из go.mod и каталог. 0.005s — время прогона, у вас будет своё число.

Имена got и want — общая привычка: так тесты пишут и в стандартной библиотеке Go, и в рабочих проектах. Не «правильный ответ» и «мой ответ», а «получено» и «хотели».


Попробуйте сейчас: функция и ручная проверка.

Цель: в scratch лежит функция price, и вы видите её результат глазами.

1. Наберите функцию в отдельный файл:

✎ Наберите в файл scratch/price.go
package main

func price(qty int) int {
	total := qty * 4500
	if qty >= 12 {
		total = total - total/10
	}
	return total
}

2. Замените заготовку scratch/main.go на вызов этой функции:

✎ Наберите в файл scratch/main.go
package main

import "fmt"

func main() {
	fmt.Println(price(3))
	fmt.Println(price(12))
}

3. Запустите:

▶ Выполните
cd ~/gocourse/day14/scratch
go run .

Готово, когда: программа напечатала две строки — 13500 и 48600. Два файла в одном каталоге — это один пакет: main.go видит price без всяких импортов, как в дне 13.


Что делает go test

Команда go test . — точка значит «пакет в текущем каталоге» — делает четыре шага:

  1. собирает пакет вместе с файлами _test.go;
  2. находит в нём все функции, чьё имя начинается с Test и у которых правильный параметр;
  3. запускает каждую такую функцию;
  4. печатает итог: ok, если ни одна не пожаловалась, и FAIL, если хоть одна пожаловалась.

Файлы _test.go попадают только в тестовую сборку. Обычная сборка их не видит — тест не увеличивает вашу программу и не попадает к пользователю:

Пример · только посмотреть, набирать не нужно
stagiaire@lab:~/gocourse/day14/scratch$ go run .
13500
48600
stagiaire@lab:~/gocourse/day14/scratch$ go build .
stagiaire@lab:~/gocourse/day14/scratch$ gofmt -l .

go run печатает то же, что печатал до появления теста, go build молча собирает программу, gofmt -l не жалуется — файл теста форматируется по тем же правилам, что и остальной код.

Второй запуск go test подряд выглядит иначе:

Пример · только посмотреть, набирать не нужно
stagiaire@lab:~/gocourse/day14/scratch$ go test .
ok  	day14/scratch	(cached)
stagiaire@lab:~/gocourse/day14/scratch$ go test -count=1 .
ok  	day14/scratch	0.009s

(cached) значит: ни один файл пакета не менялся, результат тот же, что в прошлый раз, — прогонять заново не за чем. Это не ошибка и не обман: как только вы поправите хоть один файл, тест прогонится по-настоящему. Ключ -count=1 заставляет прогнать заново в любом случае.


Попробуйте сейчас: первый тест.

Цель: go test . в scratch печатает ok.

1. Наберите тест в файл рядом с кодом:

✎ Наберите в файл scratch/price_test.go
package main

import "testing"

func TestPrice(t *testing.T) {
	got := price(3)
	want := 13500
	if got != want {
		t.Errorf("price(3) = %d, хотели %d", got, want)
	}
}

2. Запустите тест, потом ещё раз:

▶ Выполните
cd ~/gocourse/day14/scratch
go test .
go test .

3. И убедитесь, что обычный запуск программы не изменился:

▶ Выполните
go run .

Готово, когда: первый go test . напечатал ok и время, второй — ok и (cached), а go run . печатает те же две строки — пункт t_test.


Что может пойти не так

Три частые беды — и все три не про вашу логику, а про правила из таблицы выше. go test про них говорит по-разному.

Файл назван pricetest.go вместо price_test.go — тест просто не найден:

Вывод · это печатает программа, набирать не нужно
?   	day14/scratch	[no test files]

Знак ? вместо ok значит «тестов нет». Это не ошибка, код возврата нулевой — и поэтому такую беду легко не заметить.

Имя функции начинается с маленькой буквы (testPrice):

Вывод · это печатает программа, набирать не нужно
ok  	day14/scratch	0.005s [no tests to run]

Файл найден, а тестовых функций в нём нет: имя должно начинаться с Test, и следующая буква — заглавная.

Забыли import "testing":

Вывод · это печатает программа, набирать не нужно
# day14/scratch [day14/scratch.test]
./price_test.go:3:19: undefined: testing
FAIL	day14/scratch [build failed]
FAIL

Это обычная ошибка компилятора, такая же, как в дне 01: файл, строка, колонка, причина. [build failed] — «пакет даже не собрался», до запуска тестов дело не дошло.

Что видите Что это значит Что делать
? day14/scratch [no test files] в каталоге нет ни одного файла с именем на _test.go переименовать файл: price_test.go, не pricetest.go и не test_price.go
ok … [no tests to run] файл есть, а функции с именем на Test в нём нет func TestPrice(t *testing.T), с заглавной буквы после Test
./price_test.go:3:19: undefined: testing нет строки import "testing" добавить импорт
wrong signature for TestPrice, must be: func TestPrice(t *testing.T) у тестовой функции нет параметра или он не тот параметр ровно один: t *testing.T
found packages main (main.go) and price (price_test.go) в тесте написан другой package в тесте тот же пакет, что у кода: package main (в скобках Go называет первый по алфавиту файл каждого пакета, поэтому там main.go, а не price.go)
no Go files in /home/stagiaire/gocourse/day14 запустили go test . в каталоге дня, где лежит только go.mod cd в каталог задания и оттуда go test .

Попробуйте сейчас: репозиторий дня.

Цель: в ~/gocourse/day14 есть репозиторий с первым коммитом — заготовками дня. Каждое задание дальше заканчивается своим коммитом, тест коммитится вместе с кодом.

▶ Выполните
cd ~/gocourse/day14
git init
git add .
git commit -m "день 14: заготовки"
git log --oneline

Готово, когда: git log --oneline печатает одну строку с вашим сообщением. Если git просит подпись автора — git config --global user.name и user.email, как в дне 11. Что и когда коммитят — седьмая страница.


Дальше: как читать вывод упавшего теста — course next