Этот блог является частью серии Just Enough Go и предоставляет введение в тестирование Go с помощью нескольких примеров. Он охватывает основы тестирования, за которыми следуют такие темы, как подтесты, тесты на основе таблиц и т. Д.

Код доступен в репозитории Just Enough Go на GitHub

Обзор

Поддержка тестирования встроена в Go в виде testingpackage. Как минимум вам необходимо:

  • напишите код (тот, который вам нужно протестировать!), например. 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 - следите за новостями. Если вы нашли это полезным, не забудьте поставить лайк и подписаться!