Тестирование вашего кода — важный шаг в процессе разработки. Однако это может стать громоздким и занимать много времени, особенно при работе с большим количеством тестовых случаев. Копирование и вставка кода вручную для проверки каждого случая не является чистым решением.

В этой статье мы рассмотрим табличные тесты в Go, чтобы написать тест быстро и эффективно. Этот подход предлагает несколько преимуществ, включая более быстрое написание тестов, более простое обслуживание и улучшенное тестовое покрытие.

Почему таблица?

Табличное тестирование — это подход, который позволяет нам определять и выполнять несколько тестовых случаев простым способом. Идея состоит в том, чтобы создать «таблицу», состоящую из массива структур, где мы можем хранить входные данные, ожидаемые выходные данные и любую другую соответствующую информацию для каждого теста.

tests := []struct {
  // parameter
  param1 int
  param2 string
  param3 interface{} // useful for complex values
  // ...

  // results
  expected1 string
  expected2 int
  // ...

  err error
 }{}

После того, как мы определили наши тестовые примеры, мы можем выполнить итерацию по массиву и выполнить тестируемую функцию для каждого входа, проверяя, соответствует ли фактический результат ожидаемому результату для каждого случая.

for _, test := range tests {
  actual := function(test.param)
  assert.Equal(t, test.expected, actual)
}

Используя этот подход, мы можем написать более лаконичный и удобный в сопровождении тестовый код, который охватывает широкий спектр сценариев. Мы также можем легко расширить наш набор тестов, добавив в таблицу новые тестовые примеры, без необходимости дублировать тестовый код или писать новые тестовые функции.

Хотя это может потребовать некоторых первоначальных усилий, этот метод может в конечном итоге упростить и оптимизировать ваш код, сделав его более чистым и читабельным.

Простой пример

Рассмотрим функцию, которая принимает на вход электронное письмо и проверяет, действительно ли оно, возвращая логическое значение.

package main

import (
 "regexp"
)

// CheckEmail returns true if the given string is a valid email, otherwise false
func CheckEmail(email string) bool {
 pattern := `^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$`
 reg := regexp.MustCompile(pattern)
 return reg.MatchString(email)
}

В этом случае тестирования функции, которая принимает адрес электронной почты в качестве входных данных и возвращает логическое значение, мы можем определить структуру с двумя полями: одно для ввода электронной почты и одно для ожидаемого логического вывода.

При тестировании обычно сравнивают «фактическое» значение (значение, возвращаемое тестируемой функцией) с «ожидаемым» значением (значение, которое мы ожидаем, что функция вернет при определенных входных данных).

package main

import (
 "fmt"
 "testing"

 "github.com/stretchr/testify/assert"
)

func TestCheckEmail(t *testing.T) {
 tests := []struct {
  email    string
  expected bool
 }{
  {"[email protected]", true},
  {"[email protected]", true},
  {"[email protected]", true},
  {"[email protected]", true},
  {"[email protected]", false},
  {"email@email", false},
  {"email", false},
  {"@email.com", false},
  {"@email", false},
 }

 for i, test := range tests {
  actual := CheckEmail(test.email)

  msg := fmt.Sprintf("test %d with email %s", i, test.email)
  assert.Equal(t, test.expected, actual, msg)
 }
}

Действительно, как вы можете видеть в этом примере, мы можем протестировать несколько случаев всего в нескольких строках кода, используя подход на основе таблиц. Этот метод может сэкономить нам время и усилия, гарантируя, что наши тесты охватывают широкий спектр сценариев.

Более того, добавить новый тестовый пример просто, поскольку все, что нам нужно сделать, это добавить новую запись в массив с входными данными и ожидаемыми выходными данными.

Таким образом, мы можем легко расширить наш набор тестов и охватить больше пограничных случаев без необходимости писать дополнительные тестовые функции или дублировать код. Используя табличный подход, мы можем сохранить наш тестовый код чистым, кратким и удобным для сопровождения, что может сэкономить нам время и силы в долгосрочной перспективе.

Добавить собственное сообщение

В структуру можно добавить дополнительное поле для пользовательского сообщения, которое будет отображаться в случае ошибки. Это может быть особенно полезно при отладке сбоев или выявлении проблем в ваших тестах.

Например, предположим, что мы хотим добавить пользовательское сообщение об ошибке в тестовый пример, который мы обсуждали ранее. В этом случае мы могли бы изменить структуру, включив в нее поле с описательным сообщением об ошибке, которое будет отображаться в случае сбоя теста.

func TestCheckEmailWithMessage(t *testing.T) {
 tests := []struct {
  email    string
  expected bool
  message  string
 }{
  {"[email protected]", true, "standard email"},
  {"[email protected]", true, "numbers should be accepted"},
  {"[email protected]", true, "underscores should be accepted"},
  {"[email protected]", true, "dots should be accepted"},
  {"[email protected]", false, "special character not allowed"},
  {"email@email", false, "bad domain given"},
  {"email", false, "no domain given"},
  {"@email.com", false, "no username given"},
  {"@email", false, "no username and bad domain given"},
 }

 for _, test := range tests {
  actual := CheckEmail(test.email)
  assert.Equal(t, test.expected, actual, test.message)
 }
}

Обработка большого количества параметров

При работе с большим количеством параметров эффективное управление ими может стать сложной задачей. В таких ситуациях лучшим подходом может быть создание нескольких структур, каждая из которых предназначена для решения определенного аспекта проблемы. Эти структуры могут быть организованы как отдельные массивы с одинаковым количеством элементов, что упрощает управление ими.

Этот метод может оказаться особенно полезным при имитации интерфейса, когда тестируемая функция вызывает многочисленные методы. Разделив параметры на несколько структур, вы можете улучшить организацию и читабельность своего кода, упростив его обслуживание и отладку.

func TestMultipleStructs(t *testing.T) {
 func1 := []struct {
  param  string
  result int
  err    error
 }{}

 func2 := []struct {
  param1 time.Time
  param2 string
  result string
  err    error
 }{}

 tests := []struct {
  param    int
  expected string
  err      error
 }{}

 // check same number of test cases
 assert.Len(t, func1, len(func2))

 for i := range func1 {
  // do something with func1[i]
  f1 := func1[i]
  mock.Interface.EXPECT().Func1(f1.param).Return(f1.result, f1.err)

  // do something with func2[i]
  f2 := func2[i]
  mock.Interface.EXPECT().Func2(f2.param1, f2.param2).Return(f2.result, f2.err)

  // check result
  test := tests[i]
  actual, err := FunctionToTest(test.param)
  assert.Equal(t, test.expected, actual)
  if test.err == nil {
   assert.Nil(t, err)
  } else {
   assert.EqualError(t, err, test.err.Error())
  }
 }
}

В следующий раз, когда вы будете писать тест, подумайте, как вы можете его структурировать с помощью табличного подхода. В целом, если вы потратите время на структурирование своих тестов таким образом, это может привести к получению более надежного и надежного кода, что делает его достойным вложением в качество вашего программного обеспечения.