Статьи

Пойдем: тестирование программ Golang

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

Go — невероятно мощный язык программирования, изучите все от написания простых утилит до создания масштабируемых, гибких веб-серверов в нашем полном курсе.

  • Go Lang
    Основные принципы построения веб-серверов
    Дерек Дженсен

Проблема разности суммы квадратов довольно проста: «Найдите разницу между суммой квадратов первых ста натуральных чисел и квадратом суммы».

Эта конкретная проблема может быть решена довольно кратко, особенно если вы знаете своего Гаусса. Например, сумма первых N натуральных чисел равна (1 + N) * N / 2 , а сумма квадратов первых N целых чисел равна: (1 + N) * (N * 2 + 1) * N / 6 Таким образом, вся проблема может быть решена с помощью следующей формулы и присвоением 100 N:

(1 + N) * (N * 2 + 1) * N / 6 - ((1 + N) * N / 2) * ((1 + N) * N / 2)

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

Код доступен на GitHub .

Вот подписи четырех функций:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// The MakeIntList() function returns an array of consecutive integers
 
// starting from 1 all the way to the `number` (including the number)
 
func MakeIntList(number int) []int
 
 
 
// The squareList() function takes a slice of integers and returns an
 
// array of the quares of these integers
 
func SquareList(numbers []int) []int
 
 
 
// The sumList() function takes a slice of integers and returns their sum
 
func SumList(numbers []int) int
 
 
 
// Solve Project Euler #6 — Sum square difference
 
func Process(number int) int

Теперь, с нашей целевой программой (пожалуйста, прости меня, фанаты TDD), давайте посмотрим, как писать тесты для этой программы.

Пакет тестирования идет рука об руку с командой go test . Ваши тесты пакетов должны идти в файлах с суффиксом «_test.go». Вы можете разделить свои тесты на несколько файлов, которые следуют этому соглашению. Например: «what1_test.go» и «what2_test.go». Вы должны поместить свои тестовые функции в эти тестовые файлы.

Каждая тестовая функция является публично экспортируемой функцией, имя которой начинается с «Test», принимает указатель на объект testing.T и ничего не возвращает. Это выглядит как:

1
2
3
func TestWhatever(t *testing.T) {
    // Your test code goes here
}

Объект T предоставляет различные методы, которые вы можете использовать для указания сбоя или записи ошибок.

Помните: команда go test выполняет только тестовые функции, определенные в тестовых файлах.

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

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

Давайте начнем с функции SumList() . Эта функция берет часть целых чисел и возвращает их сумму. Вот тестовая функция, которая проверяет, что SumList() ведет себя как следует.

Он тестирует два тестовых случая, и, если ожидаемый результат не соответствует результату, он вызывает метод Error() объекта testing.T.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
func TestSumList_NotIdiomatic(t *testing.T) {
    // Test []{} -> 0
    result := SumList([]int{})
    if result != 0 {
            t.Error(
                «For input: «, []int{},
                «expected:», 0,
                «got:», result)
    }
 
    // Test []{4, 8, 9} -> 21
    result = SumList([]int{4, 8, 9})
    if result != 21 {
            t.Error(
                «For input: «, []int{},
                «expected:», 0,
                «got:», result)
    }
}

Это все просто, но выглядит немного многословно. Тестирование Idiomatic Go использует табличные тесты, в которых вы определяете структуру для пар входов и ожидаемых выходов, а затем получаете список этих пар, которые вы передаете в цикле к одной и той же логике. Вот как это делается для тестирования функции SumList() .

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
type List2IntTestPair struct {
    input []int
    output int
}
 
 
func TestSumList(t *testing.T) {
    var tests = []List2IntTestPair{
        {[]int{}, 0},
        {[]int{1}, 1},
        {[]int{1, 2}, 3},
        {[]int{12, 13, 25, 7}, 57},
    }
 
    for _, pair := range tests {
        result := SumList(pair.input)
        if result != pair.output {
            t.Error(
                «For input: «, pair.input,
                «expected:», pair.output,
                «got:», result)
        }
    }
}

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

Вот еще один пример для тестирования функции SquareList() . В этом случае и вход, и выход являются кусочками целых чисел, поэтому структура тестовой пары отличается, но поток идентичен. Здесь интересно то, что Go не предоставляет встроенного способа сравнения reflect.DeepEqual() , поэтому я использую reflect.DeepEqual() для сравнения выходного слайса с ожидаемым слайсом.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
type List2ListTestPair struct {
    input []int
    output []int
}
 
func TestSquareList(t *testing.T) {
    var tests = []List2ListTestPair{
        {[]int{}, []int{}},
        {[]int{1}, []int{1}},
        {[]int{2}, []int{4}},
        {[]int{3, 5, 7}, []int{9, 25, 49}},
    }
 
    for _, pair := range tests {
        result := SquareList(pair.input)
        if !reflect.DeepEqual(result, pair.output) {
            t.Error(
                «For input: «, pair.input,
                «expected:», pair.output,
                «got:», result)
        }
    }
}

Запускать тесты так же просто, как набирать go test в каталоге вашего пакета. Go найдет все файлы с суффиксом «_test.go» и все функции с префиксом «Test» и запустит их как тесты. Вот как это выглядит, когда все в порядке:

1
2
3
4
5
(G)/project-euler/6/go > go test
 
PASS
 
ok _/Users/gigi/Documents/dev/github/project-euler/6/go 0.006s

Не очень драматично. Позвольте мне специально пройти тест. Я изменю контрольный пример для SumList() таким образом, чтобы ожидаемые выходные данные для суммирования 1 и 2 составляли 7.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
func TestSumList(t *testing.T) {
    var tests = []List2IntTestPair{
        {[]int{}, 0},
        {[]int{1}, 1},
        {[]int{1, 2}, 7},
        {[]int{12, 13, 25, 7}, 57},
    }
 
    for _, pair := range tests {
        result := SumList(pair.input)
        if result != pair.output {
            t.Error(
                «For input: «, pair.input,
                «expected:», pair.output,
                «got:», result)
        }
    }
}

Теперь, когда вы набираете go test , вы получаете:

01
02
03
04
05
06
07
08
09
10
11
(G)/project-euler/6/go > go test
 
— FAIL: TestSumList (0.00s)
 
006_sum_square_difference_test.go:80: For input: [1 2] expected: 7 got: 3
 
FAIL
 
exit status 1
 
FAIL _/Users/gigi/Documents/dev/github/project-euler/6/go 0.006s

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

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

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

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

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

Go имеет встроенную поддержку для тестирования, четко определенный способ написания тестов и рекомендуемые рекомендации в виде табличных тестов.

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