Введение
Юнит-тесты являются неотъемлемой частью любой кодовой базы, в том числе и той, что находится на моем рабочем месте. Когда инженер добавляет часть новой логики в кодовую базу, от него ожидают, что он добавит модульные тесты, которые будут покрывать, по крайней мере, наиболее распространенные сценарии использования новой логики. Возможно, с парой крайних случаев в качестве вишенки на вершине.
Тестирование различных сценариев важно, но требует написания довольно повторяющихся тестов. Давайте посмотрим, какие подходы для этого предлагает Golang.
Тестирование упаковки коробки с пончиками
Мы рассмотрим два различных способа написания модульных тестов в Golang. Мы будем использовать игривый пример упаковки коробки с пончиками. Вкусы пончиков в примерах вдохновлены пончиками Crosstown. Давайте посмотрим на код, для которого мы будем писать тесты.
Реализация коробки с пончиками
package doughnuts_box
import (
"fmt"
)
type doughnutsBox struct {
capacity int
doughnuts []string
}
var knownDoughnutTypes = map[string]bool{
"Matcha Tea": true,
"Lime & Coconut (ve)": true,
"Home Made Raspberry Jam": true,
"Cinnamon Scroll (ve)": true,
"Sri Lankan Cinnamon Sugar": true,
}
func newDoughnutsBox(capacity int) *doughnutsBox {
return &doughnutsBox{
capacity: capacity,
doughnuts: make([]string, 0),
}
}
func (b *doughnutsBox) pack(doughnuts []string) (int, error) {
unrecognizedItems := make([]string, 0)
var err error
if len(doughnuts) > b.capacity {
return 0, fmt.Errorf("failed to put %d doughnuts in the box, it's only has %d doughnuts capacity", len(doughnuts), b.capacity)
}
for _, doughnut := range doughnuts {
if _, found := knownDoughnutTypes[doughnut]; found {
b.doughnuts = append(b.doughnuts, doughnut)
continue
}
unrecognizedItems = append(unrecognizedItems, doughnut)
}
if len(unrecognizedItems) > 0 {
err = fmt.Errorf("the following items cannot be placed into the box: %v", unrecognizedItems)
}
return len(b.doughnuts), err
}
Теперь, после того как мы ознакомились с реализацией коробки с пончиками, давайте обсудим, как мы можем ее протестировать.
Тестирование упаковки коробки с пончиками с помощью тестов, управляемых таблицами
Тесты, управляемые таблицами, — это распространенный способ написания модульных тестов в Golang. Вы можете встретить этот стиль повсюду.
Табличные тесты предлагают сжатый способ написания тестовых сценариев при минимальном повторении кода. Плотность синтаксиса имеет недостаток в виде плохой читабельности. Тесты могут казаться элегантными при написании, но их часто трудно читать и рассуждать о них.
Тесты в виде таблиц хороши в ситуациях, когда предполагается протестировать большое количество сценариев, с четкими ожиданиями
вход x производит выход y.
Для нашего примера с коробкой для пончиков мы написали набор сценариев в виде анонимной структуры testCases
. Затем мы запустили их все как подтесты с помощью метода t.Run
, внутри знакомого цикла for loop.
.
Реализация тестов, управляемых таблицами в коробке с пончиками
package doughnuts_box
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestPackDoughnutsBoxTableTests(t *testing.T) {
// Anonymous struct of test cases
tests := []struct {
name string
boxCapacity int
errorExpected bool
errorMessage string
items []string
expectedNumOfDoughnutsInTheBox int
}{
{
name: "Filling the box with tasty doughnuts",
boxCapacity: 4,
errorExpected: false,
errorMessage: "",
items: []string{"Sri Lankan Cinnamon Sugar", "Matcha Tea", "Home Made Raspberry Jam", "Lime & Coconut (ve)"},
expectedNumOfDoughnutsInTheBox: 4,
},
{
name: "Attempt to fill the box with too many doughnuts",
boxCapacity: 4,
errorExpected: true,
errorMessage: "failed to put 5 doughnuts in the box, it's only has 4 doughnuts capacity",
items: []string{"Sri Lankan Cinnamon Sugar", "Matcha Tea", "Home Made Raspberry Jam", "Lime & Coconut (ve)", "Lime & Coconut (ve)"},
expectedNumOfDoughnutsInTheBox: 0,
},
{
name: "Attempt to put a giant chocolate cookie into the box",
boxCapacity: 2,
errorExpected: true,
errorMessage: "the following items cannot be placed into the box: [Giant Chocolate Cookie]",
items: []string{"Sri Lankan Cinnamon Sugar", "Giant Chocolate Cookie"},
expectedNumOfDoughnutsInTheBox: 1,
},
}
for _, tc := range tests {
// each test case from table above run as a subtest
t.Run(tc.name, func(t *testing.T) {
// Arrange
box := newDoughnutsBox(tc.boxCapacity)
// Act
numOfDoughnutsInTheBox, err := box.pack(tc.items)
// Assert
if tc.errorExpected {
require.Error(t, err)
assert.Equal(t, tc.errorMessage, err.Error())
}
assert.Equal(t, tc.expectedNumOfDoughnutsInTheBox, numOfDoughnutsInTheBox)
})
}
}
Мы рассмотрели тест «счастливого пути» и два теста для ошибок, которые, как мы ожидаем, будут встречаться часто — попытка положить слишком много пончиков в коробку и попытка положить в коробку что-то, что не является пончиком.
Далее мы рассмотрим, как можно переписать те же самые тесты в другом стиле.
Тестирование упаковки пончиков в коробку с помощью отдельных подтестов
Написание сценариев тестов в виде отдельных подтестов менее распространено в Golang. Этот факт, на мой взгляд, вызывает сожаление. Но все же примеров такого синтаксиса достаточно.
Синтаксис отдельных подтестов, в отличие от табличных тестов, благоприятствует читабельности и общей «сухости кода». Тесты могут выглядеть более повторяющимися, но каждый отдельный подтест представляет собой отдельную «историю». Поэтому при использовании отдельных подтестов легче следовать принципам тестирования, ориентированного на поведение, чем при использовании тестов, ориентированных на таблицы.
В нашем небольшом примере все настройки выполняются внутри отдельных подтестов. В более сложных примерах состояние, разделяемое между подтестами, может быть инициализировано в теле родительского теста. А повторяющиеся действия по установке/разборке можно вынести в вспомогательные функции.
Реализация теста Donuts box в виде отдельных подтестов
package doughnuts_box
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestPackDoughnutsBoxSubtests(t *testing.T) {
t.Run("It fills the box with tasty doughnuts", func(t *testing.T) {
// Arrange
items := []string{"Sri Lankan Cinnamon Sugar", "Matcha Tea", "Home Made Raspberry Jam", "Lime & Coconut (ve)"}
box := newDoughnutsBox(4)
// Act
numOfDoughnutsInTheBox, err := box.pack(items)
// Assert
require.NoError(t, err)
assert.Equal(t, numOfDoughnutsInTheBox, 4)
})
t.Run("It fails to fill the box with too many doughnuts", func(t *testing.T) {
// Arrange
items := []string{"Sri Lankan Cinnamon Sugar", "Matcha Tea", "Home Made Raspberry Jam", "Lime & Coconut (ve)", "Lime & Coconut (ve)"}
box := newDoughnutsBox(4)
// Act
numOfDoughnutsInTheBox, err := box.pack(items)
// Assert
require.Error(t, err)
assert.Equal(t, "failed to put 5 doughnuts in the box, it's only has 4 doughnuts capacity", err.Error())
assert.Equal(t, 0, numOfDoughnutsInTheBox)
})
t.Run("It fails to put a giant chocolate cookie into the box", func(t *testing.T) {
// Arrange
items := []string{"Sri Lankan Cinnamon Sugar", "Giant Chocolate Cookie"}
box := newDoughnutsBox(4)
// Act
numOfDoughnutsInTheBox, err := box.pack(items)
// Assert
require.Error(t, err)
assert.Equal(t, "the following items cannot be placed into the box: [Giant Chocolate Cookie]", err.Error())
assert.Equal(t, 1, numOfDoughnutsInTheBox)
})
}
Те же три тестовых случая — счастливый путь и два распространенных сценария ошибок — мы написали в виде отдельных подтестов. В нашем примере они даже заняли примерно одинаковое количество строк кода. 58 строк для отдельных подтестов, по сравнению с 63 строками в примере с табличными тестами.
Выводы
Теперь, когда представлены примеры двух стилей для юнит-тестов, пришло время решить, какой из них использовать и когда.
Как инженер, вы должны использовать свое лучшее суждение о том, как писать модульные тесты и какой синтаксис подходит лучше всего. Вы можете использовать следующую небольшую диаграмму, которая поможет вам решить, какой синтаксис использовать.
Надеюсь, диаграмма поможет. Если у вас есть мнение, что один синтаксис лучше другого, или у вас есть свой собственный способ написания юнит-тестов на Golang, пожалуйста, поделитесь им в комментариях!