Этот блог является частью серии Just Enough Go и предоставляет введение в тестирование Go
с помощью нескольких примеров. Он охватывает основы тестирования, за которыми следуют такие темы, как подтесты, тесты на основе таблиц и т. Д.
Код доступен в репозитории Just Enough Go на
GitHub
Обзор
Поддержка тестирования встроена в Go в виде testing
package. Как минимум вам необходимо:
- напишите код (тот, который вам нужно протестировать!), например.
hello.go
- записывать тесты в файл, заканчивающийся на
_test.go
, напримерhello_test.go
- убедитесь, что имена тестовых функций начинаются с
Test_
, например,func TestHello
go test
для проведения ваших тестов!
При написании тестов вы будете активно использовать *testing.T
, который 'является типом, передаваемым тестовым функциям для управления состоянием теста и поддержки форматированных журналов тестирования.' Он содержит несколько методов, включая Error
, Fail
(варианты ) для сообщения об ошибках / сбоях, Run
для выполнения дополнительных тестов, Parallel
, Skip
и т. д.
Остальная часть блога использует простой пример, чтобы продемонстрировать некоторые из вышеупомянутых концепций. Это каноническое приложение Hello World!
package main
import "fmt"
func main() { fmt.Println(greet("")) }
func greet(who string) string { if who == "" { who = "there" } return fmt.Sprintf("hello, %s!", who) }
Привет, тесты!
Вот простой модульный тест для функции greet
:
func TestGreet(t *testing.T) {
actual := greet("abhishek")
expected := "hello, abhishek!"
if actual != expected {
t.Errorf("expected %s, but was %s", expected, actual)
}
}
Цель состоит в том, чтобы подтвердить, приводит ли вызов greet
с определенным именем к hello, <name>!
. Мы вызываем функцию greet
, сохраняем результат в переменной с именем actual
и сравниваем его со значением expected
- если они не равны, Errorf
используется для регистрации сообщения и пометки теста как неудавшегося. Однако сама функция тестирования продолжает выполняться. Если вам нужно изменить это поведение, используйте FailNow
(или _29 _ / _ 30_), чтобы завершить текущий тест и разрешить выполнение оставшихся тестов (если они есть).
Подтесты
Мы рассмотрели очевидный вариант использования, но есть еще один сценарий, который необходимо протестировать - когда вводом является пустая строка. Добавим это с помощью другого теста:
func TestGreetBlank(t *testing.T) {
actual := greet("")
expected := "hello, there!"
if actual != expected {
t.Errorf("expected %s, but was %s", expected, actual)
}
}
Альтернативой является использование sub-tests
с использованием метода Run
на *testing.T
. Вот как это будет выглядеть в этом случае:
func TestGreet2(t *testing.T) { t.Run("test blank value", func(te *testing.T) { actual := greet("") expected := "hello, there!" if actual != expected { te.Errorf("expected %s, but was %s", expected, actual) } })
t.Run("test valid value", func(te *testing.T) { actual := greet("abhishek") expected := "hello, abhishek!" if actual != expected { te.Errorf("expected %s, but was %s", expected, actual) } }) }
Логика тестирования осталась прежней. Но теперь мы рассмотрели отдельные сценарии в рамках одной функции, причем каждый сценарий представлен в виде субтеста - test blank value
и test valid value
. Метод Run
принимает name
и тестовую функцию, аналогичную родительскому тесту / функции верхнего уровня. Все тесты выполняются последовательно, и тест верхнего уровня считается завершенным, когда выполнение субтестов завершается.
Какая в этом польза? Просто не использовать отдельную функцию? Что ж, да, но есть и другие преимущества использования субтестов.
- Все случаи, связанные с функцией / методом / функциональностью, могут быть объединены в одну тестовую функцию - это значительно снижает когнитивную нагрузку.
- Явное именование значительно упрощает выявление сбоев - особенно. полезно в больших тестовых наборах
- Просто напишите (общий) код настройки и разборки до и после субтестов.
- У вас есть возможность запускать подтесты параллельно (с другими подтестами в родительском тесте) - подробнее об этом позже.
Табличные тесты
Наши тестовые примеры следуют тому же шаблону - все, что мы делаем, это вызываем greet
с аргументом и сравниваем результат с expected
. Оба субтеста имеют дублированный код. Хотя это тривиальный пример, такое случается и в реальных проектах.
table driven tests
может пригодиться в таких случаях. Все дело в поиске повторяющихся шаблонов в вашем тестовом коде и настройке tables
для определения различных комбинаций вариантов использования - это значительно сокращает дублирование тестового кода и уменьшает количество операций копирования и вставки! Эти tables
могут быть представлены как фрагменты структур, каждая из которых определяет входные параметры, ожидаемый результат, имя теста и любые другие связанные детали.
Вот как мы можем настроить table driven tests
:
func TestGreet3(t *testing.T) { type testCase struct { name string input string expectedGreeting string }
testCases := []testCase{ {name: "test blank value", input: "", expectedGreeting: "hello, there!"}, {name: "test valid value", input: "abhishek", expectedGreeting: "hello, abhishek!"}, }
for _, test := range testCases { test := test t.Run(test.name, func(te *testing.T) { actual := greet(test.input) expected := test.expectedGreeting if actual != expected { te.Errorf("expected %s, but was %s", expected, actual) } }) } }
Давайте разберемся, чтобы лучше понять. Начнем с определения структуры testCase
...
type testCase struct {
name string
input string
expectedGreeting string
}
… За которыми следуют тестовые примеры, которые представляют собой фрагмент testCase
(table
) с именем, вводом и ожидаемым выводом:
testCases := []testCase{
{name: "test blank value", input: "", expectedGreeting: "hello, there!"},
{name: "test valid value", input: "abhishek", expectedGreeting: "hello, abhishek!"},
}
Наконец, мы просто выполняем каждый из этих тестовых примеров. Обратите внимание, как имя, ввод и вывод используются с test.name
, test.input
и test.expectedGreeting
соответственно
for _, test := range testCases {
test := test
t.Run(test.name, func(te *testing.T) {
actual := greet(test.input)
expected := test.expectedGreeting
if actual != expected {
te.Errorf("expected %s, but was %s", expected, actual)
}
})
}
Параллельные тесты
В большом наборе тестов мы можем повысить эффективность, запустив все подтесты параллельно. Все, что нам нужно, чтобы сигнализировать о нашем намерении, используя метод Parallel
на *testing.T
. Вот как мы можем распараллелить оба наших тестовых примера:
обратите внимание на звонок
te.Parallel()
for _, test := range testCases {
test := test
t.Run(test.name, func(te *testing.T) {
te.Parallel()
time.Sleep(3 * time.Second)
actual := greet(test.input)
expected := test.expectedGreeting
if actual != expected {
te.Errorf("expected %s, but was %s", expected, actual)
}
})
}
Поскольку наши тесты короткие, time.Sleep
был добавлен специально, чтобы имитировать длительную операцию как часть теста. Когда вы запустите этот тест, вы заметите, что общее время выполнения теста немногим превышает 3s
, несмотря на то, что каждый тест спит в течение 3s
- это означает, что тесты выполнялись параллельно друг другу.
Другие темы
Вот еще несколько интересных тем, которые вам следует изучить, но которые не были затронуты в этом посте.
Benchmark
: Пакетtesting
предоставляет возможность запускать тесты производительности с помощью типа*testing.B
. Так же, как обычная функция тестирования начинается сTest
, тесты производительности начинаются сBenchmark
и могут быть выполнены с помощьюgo test -bench
. Они выглядят так:func BenchmarkGreet(b *testing.B)
Skip
(и его варианты): вызовите это, чтобы пропустить тест производительностиCleanup
: Тесты и тесты могут использовать это, чтобы «зарегистрировать функцию, которая будет вызываться после завершения теста и всех его подтестов»- Примеры в тестах: вы также можете включить код, который служит примерами, и тестовый пакет также их проверяет.
На этом завершается еще один выпуск из серии блогов Just Enough Go - следите за новостями. Если вы нашли это полезным, не забудьте поставить лайк и подписаться!