Зачем тест и как устроен первый
В днях 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.gopackage main func price(qty int) int { total := qty * 4500 if qty >= 12 { total = total - total/10 } return total }2. Замените заготовку
scratch/main.goна вызов этой функции:✎ Наберите в файлscratch/main.gopackage 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 . — точка значит «пакет в текущем каталоге» — делает четыре шага:
- собирает пакет вместе с файлами
_test.go; - находит в нём все функции, чьё имя начинается с
Testи у которых правильный параметр; - запускает каждую такую функцию;
- печатает итог:
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.gopackage 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