В первой статье цикла о тестировании, которая вышла почти два года назад, я описал свой подход к тестированию, который был актуален в 2016 году. Время идёт, всё изменяется, стандартная библиотека Go становится лучше и вот пару-тройку мажорных версий назад в пакете testing появился новый метод Run(), который позволяет запускать именованные подтесты. Теперь Гоблина можно отправить обратно в пещеру и уменьшить число зависимостей проекта.
Run tester Run
Не будем вспоминать, как мы без этого жили раньше, сразу же рассмотрим пример. Имеется у нас функция странного умножения:
func Multiply(a, b int) int {
res := a * b
if res == 0 {
return 0
}
if res % 2 == 0 {
return res - 1
}
return res + 1
}
Нам нужно её протестировать. Как обычно, используем табличное тестирование, но теперь мы можем вложить дополнительную информацию в таблицу и вывести её:
package multi
import (
"fmt"
"testing"
)
func TestMultiply(t *testing.T) {
testData := []struct{
name string
a, b, res int
}{
{
name: "First",
a: 1,
b: 1,
res: 2,
},
{
name: "Second",
a: 2,
b: 1,
res: 1,
},
{
name: "First",
a: 2,
b: 2,
res: 3,
},
}
for i, v := range testData {
t.Run(fmt.Sprintf("#%d name: %s", i, v.name), func(t *testing.T) {
res := Multiply(v.a, v.b)
if res != v.res {
t.Errorf("Multiply(%d, %d) => %d, expected %d", v.a, v.b, res, v.res)
}
})
}
}
Выполнив данный тест, мы увидим следующее:
go test -v
=== RUN TestMultiply
=== RUN TestMultiply/#0_name:_First
=== RUN TestMultiply/#1_name:_Second
=== RUN TestMultiply/#2_name:_First
--- PASS: TestMultiply (0.00s)
--- PASS: TestMultiply/#0_name:_First (0.00s)
--- PASS: TestMultiply/#1_name:_Second (0.00s)
--- PASS: TestMultiply/#2_name:_First (0.00s)
PASS
ok multi 0.008s
Теперь усложним нашу функцию. Предположим, мы хотим к результату кратному десяти прибавлять пять. Добавим этот случай в таблицу:
testData := []struct{
name string
a, b, res int
}{
{
name: "First",
a: 1,
b: 1,
res: 2,
},
{
name: "Second",
a: 2,
b: 1,
res: 1,
},
{
name: "First",
a: 2,
b: 2,
res: 3,
},
{
name: "Twenty",
a: 2,
b: 10,
res: 25,
},
}
Тут же видим на каком элементе наш тест завалился:
go test -v
=== RUN TestMultiply
=== RUN TestMultiply/#0_name:_First
=== RUN TestMultiply/#1_name:_Second
=== RUN TestMultiply/#2_name:_First
=== RUN TestMultiply/#3_name:_Twenty
--- FAIL: TestMultiply (0.00s)
--- PASS: TestMultiply/#0_name:_First (0.00s)
--- PASS: TestMultiply/#1_name:_Second (0.00s)
--- PASS: TestMultiply/#2_name:_First (0.00s)
--- FAIL: TestMultiply/#3_name:_Twenty (0.00s)
ttt_test.go:43: Multiply(2, 10) => 19, expected 25
FAIL
exit status 1
FAIL _/multi 0.008s
Теперь подправим нашу функцию:
func Multiply(a, b int) int {
res := a * b
if res == 0 {
return 0
}
if res % 10 == 0 {
return res + 5
}
if res % 2 == 0 {
return res - 1
}
return res + 1
}
Всё встало на свои места, все проверки пройдены:
go test -v
=== RUN TestMultiply
=== RUN TestMultiply/#0_name:_First
=== RUN TestMultiply/#1_name:_Second
=== RUN TestMultiply/#2_name:_First
=== RUN TestMultiply/#3_name:_Twenty
--- PASS: TestMultiply (0.00s)
--- PASS: TestMultiply/#0_name:_First (0.00s)
--- PASS: TestMultiply/#1_name:_Second (0.00s)
--- PASS: TestMultiply/#2_name:_First (0.00s)
--- PASS: TestMultiply/#3_name:_Twenty (0.00s)
PASS
ok multi 0.008s
Покрытие
Не смотря на то, что проверки в тесте выполнены успешно, мы не можем с уверенностью сказать, что протестировали все возможные результаты. Значительно повысить (либо наоборот) уверенность нам поможет ключ -cover
команды go test
. Запустим выполнение тестов нашего примера с ключом -cover
:
go test -cover
PASS
coverage: 87.5% of statements
ok multi 0.009s
Да, опасения были небеспочвенны, у нас покрыто только 87.5%. Наш старый знакомый GoConvey в очень удобном виде покажет какие именно части кода не покрыты тестами:
У нас не отработан кейс, при котором произведение получается равным нулю, следует его добавить:
testData := []struct{
name string
a, b, res int
}{
{
name: "First",
a: 1,
b: 1,
res: 2,
},
{
name: "Second",
a: 2,
b: 1,
res: 1,
},
{
name: "First",
a: 2,
b: 2,
res: 3,
},
{
name: "Twenty",
a: 2,
b: 10,
res: 25,
},
{
name: "Zero",
a: 2,
b: 0,
res: 0,
},
}
Проверяем покрытие:
go test -cover
PASS
coverage: 100.0% of statements
ok multi 0.007s
С таким покрытием не стыдно и в продакшн!
Немного о проверках
В стандартном пакете testing нет никаких инструментов для проведения проверок, т.н. ассертов (assert). Многие рекомендуют использовать пакет assert из поставки testify. Я для себя решил, что обойдусь. Виной тому несколько причин:
Сложнее читать код
Сравним:
assert.Equal(t, a, b, "The two words should be the same.")
// и
if b != a {
t.Errorf("got %s, want %s", b, a)
}
Если не знать API testify, не сразу догадаешься, что к чему. Обычное сравнение и вывод ошибки гораздо нагляднее, не смотря на то, что более громоздко выглядит.
Не канонично
Обратимся к официальным комментариям к коду, в частости о проверках в тестах:
Some test frameworks encourage writing these backwards: 0 != x, “expected 0, got x”, and so on. Go does not. (Некоторые тест-фреймворки подстрекают писать проверки в обратном порядке: 0 != x, “хотели 0, получили x” и в таком духе. Go не такой!)
Посмотрим на сигнатуру метода Equal:
func Equal(t TestingT, expected, actual interface{}, msgAndArgs ...interface{}) bool
Видим, что первым аргументом идёт ожидаемый результат, а лишь затем полученный, что идёт в разрез с рекомендациями. И тут не трудно запутиться. Какзалось бы, для проверки на равенство не имеет значение что с чем сравнивать, но, в случае, если проверка завалится, вывод в консоль может быть некорректным, что только добавит путаницы.
Именно поэтому я не использую testify, а пишу размашисто, зато более понятно.