1. Введение в модульное тестирование
Модульное тестирование предполагает проверку и валидацию наименьшей тестируемой единицы в программе, такой как функция или метод на языке Go. Модульное тестирование гарантирует, что код работает как ожидается, и позволяет разработчикам вносить изменения в код, не случайно нарушая существующий функционал.
В проекте на Golang важность модульного тестирования говорит сама за себя. Во-первых, это позволяет повысить качество кода, давая разработчикам больше уверенности в его изменении. Во-вторых, модульное тестирование может служить документацией для кода, объясняя его ожидаемое поведение. Кроме того, автоматический запуск модульных тестов в среде непрерывной интеграции позволяет быстро обнаруживать вновь введенные ошибки, тем самым повышая стабильность программного обеспечения.
2. Проведение базовых тестов с использованием пакета testing
В стандартной библиотеке языка Go есть пакет testing
, который предоставляет инструменты и функциональность для написания и запуска тестов.
2.1 Создание вашего первого тестового случая
Для написания тестовой функции необходимо создать файл с суффиксом _test.go
. Например, если ваш исходный файл кода называется calculator.go
, ваш тестовый файл должен называться calculator_test.go
.
Затем пришло время создать тестовую функцию. Тестовая функция должна импортировать пакет testing
и иметь определенный формат. Вот простой пример:
// calculator_test.go
package calculator
import (
"testing"
"fmt"
)
// Проверка функции сложения
func TestAdd(t *testing.T) {
result := Add(1, 2)
expected := 3
if result != expected {
t.Errorf("Ожидалось %v, но получено %v", expected, result)
}
}
В этом примере TestAdd
- это тестовая функция, которая проверяет вымышленную функцию Add
. Если результат функции Add
соответствует ожидаемому результату, тест пройдет; в противном случае будет вызвано t.Errorf
для записи информации о неудачном тесте.
2.2 Понимание правил и сигнатуры именования тестовых функций
Тестовые функции должны начинаться с Test
, за которым может следовать любая строка в нижнем регистре, и их единственным параметром должен быть указатель на testing.T
. Как показано в примере, TestAdd
следует правильным правилам именования и сигнатуре.
2.3 Запуск тестовых случаев
Вы можете запустить свои тестовые случаи, используя командную строку. Для запуска конкретного тестового случая выполните следующую команду:
go test -v // Запустить тесты в текущем каталоге и отобразить подробный вывод
Если вы хотите запустить определенный тестовый случай, вы можете использовать флаг -run
, за которым следует регулярное выражение:
go test -v -run TestAdd // Запустить только тестовую функцию TestAdd
Команда go test
автоматически найдет все файлы _test.go
и выполнит каждую тестовую функцию, удовлетворяющую критериям. Если все тесты пройдут, вы увидите сообщение, аналогичное PASS
в командной строке; если какой-либо тест не пройдет, вы увидите FAIL
вместе с соответствующим сообщением об ошибке.
3. Написание тестовых случаев
3.1 Сообщение об ошибках с использованием t.Errorf
и t.Fatalf
В языке Go тестовый фреймворк предоставляет различные методы для сообщения об ошибках. Два самых часто используемых метода - Errorf
и Fatalf
, оба из которых являются методами объекта testing.T
. Errorf
используется для сообщения об ошибках в тесте, но не останавливает текущий тестовый случай, в то время как Fatalf
останавливает текущий тест немедленно после сообщения об ошибке. Важно выбирать соответствующий метод в зависимости от требований тестирования.
Пример использования Errorf
:
func TestAdd(t *testing.T) {
got := Add(1, 2)
want := 3
if got != want {
t.Errorf("Add(1, 2) = %d; ожидалось %d", got, want)
}
}
Если вы хотите немедленно остановить тест при обнаружении ошибки, вы можете использовать Fatalf
:
func TestSubtract(t *testing.T) {
got := Subtract(5, 3)
if got != 2 {
t.Fatalf("Subtract(5, 3) = %d; ожидалось 2", got)
}
}
В целом, если ошибка может привести к неправильному выполнению последующего кода или неудача в тестировании может быть подтверждена заранее, рекомендуется использовать Fatalf
. В противном случае рекомендуется использовать Errorf
для получения более подробного результата тестирования.
3.2 Организация подтестов и запуск подтестов
В Go мы можем использовать t.Run
для организации подтестов, что помогает нам писать тестовый код более структурированно. Подтесты могут иметь свой собственный Setup
и Teardown
и могут быть запущены индивидуально, обеспечивая большую гибкость. Это особенно полезно для проведения сложных тестов или параметризованных тестов.
Пример использования подтеста t.Run
:
func TestMultiply(t *testing.T) {
testcases := []struct {
name string
a, b, expected int
}{
{"2x3", 2, 3, 6},
{"-1x-1", -1, -1, 1},
{"0x4", 0, 4, 0},
}
for _, tc := range testcases {
t.Run(tc.name, func(t *testing.T) {
if got := Multiply(tc.a, tc.b); got != tc.expected {
t.Errorf("Multiply(%d, %d) = %d; ожидалось %d", tc.a, tc.b, got, tc.expected)
}
})
}
}
Если мы хотим запустить подтест с именем "2x3" индивидуально, мы можем выполнить следующую команду в командной строке:
go test -run TestMultiply/2x3
Пожалуйста, обратите внимание, что имена подтестов чувствительны к регистру.
4. Подготовительные работы до и после тестирования
4.1 Подготовка и завершение
При проведении тестов нам часто нужно подготовить некоторое начальное состояние для тестов (например, подключение к базе данных, создание файлов и т. д.), и аналогично, нам нужно выполнить некоторую очистку после завершения тестов. В Go мы обычно выполняем Setup
и Teardown
непосредственно в функциях тестов, и функция t.Cleanup
предоставляет нам возможность регистрировать функции обратного вызова для очистки.
Вот простой пример:
func TestDatabase(t *testing.T) {
db, err := SetupDatabase()
if err != nil {
t.Fatalf("настройка не удалась: %v", err)
}
// Регистрируем функцию очистки для закрытия соединения с базой данных после завершения теста
t.Cleanup(func() {
if err := db.Close(); err != nil {
t.Errorf("не удалось закрыть базу данных: %v", err)
}
})
// Выполняем тест...
}
В функции TestDatabase
мы сначала вызываем функцию SetupDatabase
для настройки тестовой среды. Затем мы используем t.Cleanup()
, чтобы зарегистрировать функцию, которая будет вызвана после завершения теста для выполнения очистки, в данном примере - закрытие соединения с базой данных. Таким образом, мы можем обеспечить правильное освобождение ресурсов независимо от того, прошел ли тест успешно или нет.
5. Повышение эффективности тестирования
Повышение эффективности тестирования помогает нам быстрее выполнять итерации разработки, быстрее обнаруживать проблемы и обеспечивать качество кода. Ниже мы обсудим покрытие тестов, тесты на основе таблиц и использование заглушек для повышения эффективности тестирования.
5.1 Покрытие тестов и связанные инструменты
Инструмент go test
предоставляет очень полезную функцию покрытия тестов, которая помогает нам понять, какие части кода покрываются тестовыми случаями, тем самым обнаруживая области кода, которые не покрываются тестовыми случаями.
Используя команду go test -cover
, можно увидеть текущий процент покрытия тестов:
go test -cover
Если вам нужно более подробно понять, какие строки кода были выполнены, а какие - нет, вы можете использовать параметр -coverprofile
, который генерирует файл данных о покрытии. Затем вы можете использовать команду go tool cover
для создания подробного отчета о покрытии тестов.
go test -coverprofile=coverage.out
go tool cover -html=coverage.out
Вышеуказанная команда откроет веб-отчет, визуально показывающий, какие строки кода покрыты тестами, а какие - нет. Зеленый цвет представляет протестированный код, а красный - строки кода, не покрытые тестами.
5.2 Использование моков
При тестировании мы часто сталкиваемся с ситуациями, когда нам нужно имитировать внешние зависимости. Моки могут помочь нам симулировать эти зависимости, устраняя необходимость полагаться на конкретные внешние сервисы или ресурсы в среде тестирования.
В сообществе Go существует множество инструментов для создания моков, таких как testify/mock
и gomock
. Эти инструменты обычно предоставляют серию API для создания и использования мок-объектов.
Вот простой пример использования testify/mock
. Сначала необходимо определить интерфейс и его мок-версию:
type DataService interface {
FetchData() (int, error)
}
type MockDataService struct {
mock.Mock
}
func (m *MockDataService) FetchData() (int, error) {
args := m.Called()
return args.Int(0), args.Error(1)
}
При тестировании мы можем использовать MockDataService
для замещения фактического сервиса данных:
func TestSomething(t *testing.T) {
mockDataSvc := new(MockDataService)
mockDataSvc.On("FetchData").Return(42, nil) // Настройка ожидаемого поведения
result, err := mockDataSvc.FetchData() // Использование мок-объекта
assert.NoError(t, err)
assert.Equal(t, 42, result)
mockDataSvc.AssertExpectations(t) // Проверка выполнения ожидаемого поведения
}
С помощью этого подхода мы можем избежать зависимости от внешних сервисов, вызовов базы данных и т. д. в тестировании. Это может ускорить выполнение тестов и сделать их более стабильными и надежными.
6. Техники расширенного тестирования
После освоения основ тестирования модулей в Go можно изучить более продвинутые техники тестирования, которые помогут создавать более надежное программное обеспечение и повысить эффективность тестирования.
6.1 Тестирование приватных функций
В языке Golang приватные функции обычно относятся к непубличным функциям, то есть функциям, имена которых начинаются с маленькой буквы. Обычно мы предпочитаем тестировать публичные интерфейсы, так как они отражают возможности использования кода. Однако есть случаи, когда имеет смысл также напрямую тестировать приватные функции, например, когда у приватной функции сложная логика и она вызывается несколькими публичными функциями.
Тестирование приватных функций отличается от тестирования публичных функций, потому что на них нельзя ссылаться извне пакета. Распространенной техникой является написание тестового кода в том же пакете, что позволяет обращаться к приватной функции.
Вот простой пример:
// calculator.go
package calculator
func add(a, b int) int {
return a + b
}
Соответствующий файл теста выглядит следующим образом:
// calculator_test.go
package calculator
import "testing"
func TestAdd(t *testing.T) {
expected := 4
actual := add(2, 2)
if actual != expected {
t.Errorf("ожидалось %d, получено %d", expected, actual)
}
}
Поместив файл теста в тот же пакет, мы можем напрямую тестировать функцию add
.
6.2 Общие шаблоны тестирования и лучшие практики
В модульном тестировании на языке Golang существуют общие практики, которые облегчают работу по тестированию и помогают поддерживать ясность кода и его поддерживаемость.
-
Тестирование через таблицу
Тестирование через таблицу - это метод организации входных данных и ожидаемых результатов теста. Определяя набор тестов и затем выполняя их в цикле, этот метод упрощает добавление новых тестов и делает код более легким для чтения и поддержки.
// calculator_test.go
package calculator
import "testing"
func TestAddTableDriven(t *testing.T) {
var tests = []struct {
a, b int
want int
}{
{1, 2, 3},
{2, 2, 4},
{5, -1, 4},
}
for _, tt := range tests {
testname := fmt.Sprintf("%d,%d", tt.a, tt.b)
t.Run(testname, func(t *testing.T) {
ans := add(tt.a, tt.b)
if tt.want != ans {
t.Errorf("получено %d, ожидалось %d", ans, tt.want)
}
})
}
}
-
Использование моков для тестирования
Использование моков - это техника тестирования, которая заключается в замене зависимостей для тестирования различных частей функциональности. В языке Golang основным способом реализации моков являются интерфейсы. Путем использования интерфейсов можно создавать мок-реализацию и затем использовать ее для тестирования.