Пять простых советов для написания юнит-тестов на Golang
Practical.DEVТестовая разработка – это отличный способ сохранить качество вашего кода на высоком уровне, защитить себя от регрессии и доказать себе и окружающим, что ваш код делает то, что он должен.
Вот пять советов, которые помогут улучшить ваши тесты.
1. Поместите тесты в другой пакет
Go требует, чтобы файлы в одной папке принадлежали одному и тому же пакету, за исключением файлов ‘_test.go’. Перемещение тестового кода из пакета позволяет писать тесты так, как если бы вы были реальным пользователем данного пакета. У вас нет возможности возиться с внутренним устройством, но вместо этого вы фокусируетесь на беззащитном интерфейсе и всегда думаете о любом шуме, который вы можете добавить в свой API.

Это дает вам свободу в изменении внутреннего устройства как вам хочется, без необходимости каждый раз настраивать тестовый код.
2. Внутренние тесты перемещаются в другой файл
Если вам необходимо модульное тестирование некоторых внутренних компонентов, создайте другой файл с суффиксом _internal_test.go’. Внутренние тесты обязательно будут более хрупкими, чем интерфейсные, но они являются отличным способом понять поведение внутренних компонентов и особенно полезны, если вы выполняете тестовую разработку.

3. Выполнение всех тестов при сохранении
Go выполняет построение кода и работает очень быстро, так что нет причин не запускать весь набор тестов каждый раз, когда вы нажмете Save.
Это можно сделать в Sublime Text, установив GoSublime и нажав ‘Cmd+., 5’ перед добавлением следующих элементов конфигурации:
“on_save”: [{
“cmd”: “gs9o_run_many”, “args”: {
“commands”:[
[“clear”],
[“sh”, “if [ -f onsave.sh ]; then ./onsave.sh; else gofmt -s -w ./ && go build . errors && go test -i && go test && go vet && golint ; fi”]
],
“focus_view”: false
}
}],
“fmt_cmd”: [“goimports”]
Приведенный выше сценарий сначала проверяет, имеет ли проект файл onsave.sh, который будет запущен вместо него. Это позволяет легко отключить функцию автоматического тестирования пакетов, для которых это не уместно.
4. Тесты, управляемые таблицей записи
Анонимные структуры и составные литералы позволяют писать очень четкие и простые табличные тесты, не полагаясь на внешний пакет.
Следующий код позволяет нам настроить ряд тестов для еще не написанной функции Fib:
var fibTests = []struct {
n int // input
expected int // expected result
}{
{1, 1},
{2, 1},
{3, 2},
{4, 3},
{5, 5},
{6, 8},
{7, 13},
}
Затем наша тестовая функция просто перебирает тесты, вызывая метод Fib для каждого n, прежде чем утверждать, что результаты верны:
func TestFib(t *testing.T) {
for _, tt := range fibTests {
actual := Fib(tt.n)
if actual != tt.expected {
t.Errorf("Fib(%d): expected %d, actual %d", tt.n, tt.expected, actual)
}
}
}
5. Имитируйте вещи, используя код Go
Если вам нужно для правильного тестирования имитировать что-то, на что опирается ваш код, то, скорее всего интерфейс - хороший кандидат. Даже если вы полагаетесь на внешний пакет, который вы не можете менять, ваш код все равно может использовать интерфейс, удовлетворяющий внешним типам.
После нескольких лет написания макетов-пустышек я, наконец, нашел идеальный способ имитировать интерфейсы. Я сделал инструмент для написания кода без необходимости добавлять какие-либо зависимости к проекту. Оцените Moq: https://medium.com/@matryer/meet-moq-easily-mock-interfaces-in-go-476444187d10 .
Предположим, мы импортируем этот внешний пакет:
package mailman
import “net/mail”
type MailMan struct{}
func (m *MailMan) Send(subject, body string, to ...*mail.Address) {
// some code
}
func New() *MailMan {
return &MailMan{}
}
Если код, который мы тестируем, принимает объект MailMan, единственный способ вызвать его — предоставить фактический экземпляр MailMan.
func SendWelcomeEmail(m *MailMan, to ...*mail.Address) {...}
Это означает, что всякий раз, когда мы запускаем наши тесты, может быть отправлено реальное письмо. Представьте, что мы реализовали тестирование по сохранению из пункта 3. Мы бы раздражали наших тестовых пользователей или получали большие счета за обслуживание.
Альтернативой является добавление этого простого интерфейса в ваш код:
type EmailSender interface{
Send(subject, body string, to ...*mail.Address)
}
Конечно, MailMan уже удовлетворяет этому интерфейсу, так как мы взяли сигнатуру метода Send от него в первую очередь — поэтому мы все еще можем передавать объекты MailMan как и раньше.
Но теперь мы можем написать тестовый email отправителю:
type testEmailSender struct{
lastSubject string
lastBody string
lastTo []*mail.Address
}
// make sure it satisfies the interface
var _ package.EmailSender = (*testEmailSender)(nil)
func (t *testEmailSender) Send(subject, body string, to ...*mail.Address) {
t.lastSubject = subject
t.lastBody = body
t.lastTo = to
}
После этого мы можем обновить функцию SendWelcomeEmail, чтобы использовать интерфейс, а не конкретный тип:
func SendWelcomeEmail(m EmailSender, to ...*mail.Address) {...}
В нашем тестовом коде мы можем отправить вместо нашего отправителя поддельного и делать утверждения о правильности завершения тестов после вызова целевой функции:
func TestSendWelcomeEmail(t *testing.T) {
sender := &testEmailSender{}
SendWelcomeEmail(sender, to1, to2)
if sender.lastSubject != "Welcome" {
t.Error("Subject line was wrong")
}
if sender.To[0] != to1 && sender.To[1] != to2 {
t.Error("Wrong recipients")
}
}
- Если вы хотите исследовать имитации дальше, то не забудьте почитать про инструмент Moq — он не только описывает отличный способ написания имитаций, но и также пишет их для вас.