В этом уроке мы поговорим о тестировании в Go. Итак, начнем с простого примера.
Мы создали пакет math
, который содержит функцию Add
, которая, как следует из названия, складывает два целых числа.
package math
func Add(a, b int) int {
return a + b
}
Она используется в нашем пакете main
следующим образом.
package main
import (
"example/math"
"fmt"
)
func main() {
result := math.Add(2, 2)
fmt.Println(result)
}
И если мы запустим это, то увидим результат.
$ go run main.go
4
Теперь мы хотим протестировать нашу функцию Add
. Поэтому в Go мы объявляем файлы тестов с суффиксом _test
в имени файла. Поэтому для нашего add.go
мы создадим тест add_test.go
. Структура нашего проекта должна выглядеть следующим образом.
.
├── go.mod
├── main.go
└── math
├── add.go
└── add_test.go
Мы начнем с использования пакета math_test
и импорта пакета testing
из стандартной библиотеки. Именно так! Тестирование встроено в Go, в отличие от многих других языков.
Но подождите… почему мы должны использовать math_test
в качестве нашего пакета, разве мы не можем просто использовать тот же пакет math
?
Да, мы можем написать наш тест в том же пакете, если захотим, но я лично считаю, что выполнение теста в отдельном пакете помогает нам писать тесты более развязанным способом.
Теперь мы можем создать нашу функцию TestAdd
. Она будет принимать аргумент типа testing.T
, который предоставит нам полезные методы.
package math_test
import "testing"
func TestAdd(t *testing.T) {}
Прежде чем добавлять логику тестирования, давайте попробуем запустить его. Но на этот раз мы не можем использовать команду go run
, вместо этого мы воспользуемся командой go test
.
$ go test ./math
ok example/math 0.429s
Здесь у нас будет имя нашего пакета math
, но мы также можем использовать относительный путь ./...
для проверки всех пакетов.
$ go test ./...
? example [no test files]
ok example/math 0.348s
И если Go не найдет ни одного теста в пакете, он сообщит нам об этом.
Отлично, давайте напишем тестовый код. Для этого мы проверим наш результат с ожидаемым значением, и если они не совпадут, мы можем использовать метод t.Fail
, чтобы провалить тест.
package math_test
import "testing"
func TestAdd(t *testing.T) {
got := math.Add(1, 1)
expected := 2
if got != expected {
t.Fail()
}
}
Отлично! Похоже, наш тест прошел.
$ go test math
ok example/math 0.412s
Давайте также посмотрим, что произойдет, если мы провалим тест, для этого мы можем изменить наш ожидаемый результат.
package math_test
import "testing"
func TestAdd(t *testing.T) {
got := math.Add(1, 1)
expected := 3
if got != expected {
t.Fail()
}
}
$ go test ./math
ok example/math (cached)
Если вы видите это, не волнуйтесь. Для оптимизации наши тесты кэшируются. Мы можем использовать команду go clean
, чтобы очистить кэш, а затем повторно запустить тест.
$ go clean -testcache
$ go test ./math
--- FAIL: TestAdd (0.00s)
FAIL
FAIL example/math 0.354s
FAIL
Итак, вот как будет выглядеть сбой теста.
Тесты, управляемые таблицами
Это подводит нас к тестам, управляемым таблицами. Но что это такое?
Итак, ранее у нас были аргументы функций и ожидаемые переменные, которые мы сравнивали, чтобы определить, прошли наши тесты или нет. Но что если мы определим все это в виде фрагмента и будем выполнять итерации по нему? Это сделает наши тесты немного более гибкими и поможет нам легко запускать несколько случаев.
Не волнуйтесь, мы научимся этому на примере. Итак, мы начнем с определения нашей структуры addTestCase
.
package math_test
import (
"example/math"
"testing"
)
type addTestCase struct {
a, b, expected int
}
var testCases = []addTestCase{
{1, 1, 3},
{25, 25, 50},
{2, 1, 3},
{1, 10, 11},
}
func TestAdd(t *testing.T) {
for _, tc := range testCases {
got := math.Add(tc.a, tc.b)
if got != tc.expected {
t.Errorf("Expected %d but got %d", tc.expected, got)
}
}
}
Обратите внимание, как мы объявили addTestCase
с нижнего регистра. Правильно, мы не хотим экспортировать его, поскольку он не полезен за пределами нашей логики тестирования. Давайте запустим наш тест.
$ go run main.go
--- FAIL: TestAdd (0.00s)
add_test.go:25: Expected 3 but got 2
FAIL
FAIL example/math 0.334s
FAIL
Похоже, что наши тесты сломались, давайте исправим их, обновив наши тестовые примеры.
var testCases = []addTestCase{
{1, 1, 2},
{25, 25, 50},
{2, 1, 3},
{1, 10, 11},
}
Отлично, все работает!
$ go run main.go
ok example/math 0.589s
Покрытие кода
Наконец, давайте поговорим о покрытии кода. При написании тестов часто важно знать, какую часть кода покрывают тесты. Это обычно называется покрытием кода.
Для расчета и экспорта покрытия для нашего теста мы можем просто использовать аргумент -coverprofile
с командой go test
.
$ go test ./math -coverprofile=coverage.out
ok example/math 0.385s coverage: 100.0% of statements
Похоже, у нас отличное покрытие. Давайте также проверим отчет с помощью команды go tool cover
, которая дает нам подробный отчет.
$ go tool cover -html=coverage.out
Как мы видим, это гораздо более читабельный формат. И что самое приятное, он встроен прямо в стандартный инструментарий.
Фазз-тестирование
Наконец, давайте рассмотрим фазз-тестирование, которое было введено в Go версии 1.18.
Фаззинг — это тип автоматизированного тестирования, который непрерывно манипулирует входными данными программы для поиска ошибок.
Go fuzzing использует руководство по покрытию для интеллектуального прохождения по проверяемому коду, чтобы найти и сообщить пользователю о сбоях.
Поскольку он может достигать краевых случаев, которые человек часто пропускает, фазз-тестирование может быть особенно ценным для поиска ошибок и эксплойтов безопасности.
Попробуем рассмотреть пример:
func FuzzTestAdd(f *testing.F) {
f.Fuzz(func(t *testing.T, a, b int) {
math.Add(a , b)
})
}
Если мы запустим этот пример, то увидим, что он автоматически создаст тестовые случаи. Поскольку наша функция Add
довольно проста, тесты пройдут.
$ go test -fuzz FuzzTestAdd example/math
fuzz: elapsed: 0s, gathering baseline coverage: 0/192 completed
fuzz: elapsed: 0s, gathering baseline coverage: 192/192 completed, now fuzzing with 8 workers
fuzz: elapsed: 3s, execs: 325017 (108336/sec), new interesting: 11 (total: 202)
fuzz: elapsed: 6s, execs: 680218 (118402/sec), new interesting: 12 (total: 203)
fuzz: elapsed: 9s, execs: 1039901 (119895/sec), new interesting: 19 (total: 210)
fuzz: elapsed: 12s, execs: 1386684 (115594/sec), new interesting: 21 (total: 212)
PASS
ok foo 12.692s
Но если мы обновим нашу функцию Add
со случайным краевым случаем, например, программа запаникует, если b + 10
будет больше, чем a
.
func Add(a, b int) int {
if a > b + 10 {
panic("B must be greater than A")
}
return a + b
}
И если мы повторно запустим тест, этот краевой случай будет пойман с помощью fuzz-тестирования.
$ go test -fuzz FuzzTestAdd example/math
warning: starting with empty corpus
fuzz: elapsed: 0s, execs: 0 (0/sec), new interesting: 0 (total: 0)
fuzz: elapsed: 0s, execs: 1 (25/sec), new interesting: 0 (total: 0)
--- FAIL: FuzzTestAdd (0.04s)
--- FAIL: FuzzTestAdd (0.00s)
testing.go:1349: panic: B is greater than A
Я считаю, что это очень хорошая функция Go 1.18. Вы можете узнать больше о фазз-тестировании в официальном блоге Go.
Эта статья является частью моего открытого курса по Go, доступного на Github.
karanpratapsingh / go-course
Освойте основы и расширенные возможности языка программирования Go
Курс по Go
Привет, добро пожаловать на курс, и спасибо за изучение Go. Я надеюсь, что этот курс обеспечит вам отличный опыт обучения.
Этот курс также доступен на моем сайте, а также на Educative.io
Оглавление
-
Начало работы
- Что такое Go?
- Зачем изучать Go?
- Установка и настройка
-
Глава I
- Hello World
- Переменные и типы данных
- Форматирование строк
- Управление потоком данных
- Функции
- Модули
- Пакеты
- Рабочие пространства
- Полезные команды
- Сборка
-
Глава II
- Указатели
- Структуры
- Методы
- Массивы и фрагменты
- Карты
-
Глава III
- Интерфейсы
- Ошибки
- Паника и восстановление
- Тестирование
- Дженерики
-
Глава IV
- Параллелизм
- Гороутины
- Каналы
- Выбрать
- Пакет синхронизации
- Расширенные шаблоны параллелизма
- Контекст
-
Приложение
- Следующие шаги
- Ссылки
Что такое Go?
Go (также известный как Golang) — это язык программирования, разработанный в Google в 2007 году и открытый в 2009 году.
Основное внимание в нем уделяется простоте, надежности и эффективности. Он был разработан, чтобы объединить эффективность, скорость и безопасность статически типизированного и компилируемого языка с легкостью…