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

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

Итак, приступим.

Код

Функциональность, которую мы тестируем сегодня, довольно проста. У нас есть интерфейс Operator, который имеет два метода.

Generate принимает два целых числа и создает ключ на основе некоторого шаблона, а Degenerate берет ключ и декодирует целые числа, из которых был создан ключ.

Если ключ недействителен, Degenerate выдает ошибку.

Теперь давайте создадим реализацию этого интерфейса под названием keyOp.

Логика здесь довольно проста.

Мы предоставляем функцию GetKeyOperator, которая создает новый экземпляр keyOp с шаблоном %v_%v.

Generate() объединяет два целых числа в шаблон.

Degenerate() преобразует ключ в соответствующие два целых числа. Если ключ не соответствует структуре шаблона, которая определяется позицией символа _, мы возвращаем ошибку.

Теперь функция main может использовать эти методы для просмотра функциональности.

Как и ожидалось, результат такой:

key=2_3, a=2, b=3

Теперь, когда мы установили функциональность. Приступим к тестированию.

Тестирование

Сначала мы создадим файл с именем operators_test.go в том же каталоге, что и operators.go. Go автоматически ищет файлы в каталоге с суффиксом _test при выполнении команды go test.

Теперь давайте напишем тестовые примеры для наших Generate() и Degenerate() функций.

Для тестовых функций начните имя функции с Test_, а затем предоставьте структуру для этого теста. Затем вы захотите написать название метода. Например, наши тестовые методы будут называться Test_keyOp_Generate и Test_keyOp_Degenerate соответственно.

Все методы тестирования должны принимать параметр testing.T. T - это тип, передаваемый функциям тестирования для управления состоянием тестирования и поддержки форматированных журналов тестирования.

Итак, сигнатура нашего метода такова:

func Test_keyOp_Generate(t *testing.T)

Аргументы для Generate - два целых числа. Итак, давайте определим args для хранения этих двух целых чисел.

type args struct {
   x int
   y int
}

Теперь давайте определимся с требованиями нашего теста.

Мы структурируем тесты по их имени, а также аргументам в пользу теста и желаемому результату. Мы также добавим несколько таких тестов:

tests := []struct {
   name string
   args args
   want string
}{
   {
      name: "success",
      args: args{
         x: 5,
         y: 50,
      },
      want: "5_50",
   },
   {
      name: "success large integers",
      args: args{
         x: 50000,
         y: 999999,
      },
      want: "50000_999999",
   },
}

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

Теперь мы можем просто перебрать test срез и запустить тесты.

for _, tt := range tests {
   t.Run(tt.name, func(t *testing.T) {
      kp := GetKeyOperator()
      if got := kp.Generate(tt.args.x, tt.args.y); got != tt.want {
         t.Errorf("keyOp.Generate() = %v, want %v", got, tt.want)
      }
   })
}

Мы используем метод Run, указанный в структуре testing.T. Run принимает имя теста и функцию (func (t *testing.T)) в качестве входных параметров и возвращает bool. Он запускает функцию, указанную во втором параметре, в отдельной горутине и блокируется, пока не вернется.

В условии if мы создаем и экземпляр kp из keyOp и проверяем, получили ли мы ожидаемый результат, проверяя got!=want. Если тестовый пример терпит неудачу, мы записываем сообщение об ошибке в объект testing.T, используя t.Errorf() и правильно структурируя наше сообщение об ошибке.

Наша полная функция тестирования для Generate находится здесь.

Теперь давайте попробуем проделать то же самое с методом Degenerate(). Изменения здесь заключаются в том, что args будет принимать только один string в качестве параметра.

Кроме того, поскольку мы возвращаем три результата из Degenerate(), мы проверим их с помощью переменных wantX, wantY и wantErr. wantErr - это bool, который определит, получили ли мы ненулевую ошибку от тестируемого метода.

type args struct {
   s string
}
tests := []struct {
   name    string
   args    args
   wantX   int
   wantY   int
   wantErr bool
}{
   {
      name: "success",
      args: args{
         s: "40_99",
      },
      wantX: 40,
      wantY: 99,
   },
   {
      name: "failure",
      args: args{
         s: "4099",
      },
      wantErr:true,
   },
}

Как видите, мы добавили два простых тестовых случая: один для успешной проверки, а другой, имеющий недопустимые входные данные, для проверки ошибки. В тесте success мы проверяем правильные значения x и y с помощью wantX и wantY, в то время как в тесте failure мы проверяем ненулевую ошибку с помощью wantErr.

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

Запуск тестовых случаев

Мы можем запустить наши тестовые примеры с помощью команды test. go test имеет множество функций, которые можно найти в его подробной документации здесь.

Самое простое использование go test - запускать все тесты в текущем каталоге и во всех подкаталогах. Это можно сделать, выполнив следующую команду:

go test ./...

Заключение

Я надеюсь, что это руководство послужит простым введением в методы модульного тестирования Golang.