Все знают, что 100% покрытия кода не существует и не добавляет никакой добавленной стоимости. Фактически, каждый день мы действительно хотим проверить нашу бизнес-логику, интеллект нашего приложения.
В этой статье мы начнем с небольшого CLI-приложения на основе Go, у которого еще нет модульных тестов, а затем проведем несколько модульных тестов gRPC .
Давайте начнем с рассмотрения нашего приложения.
Разработка CLI в Go — это детская игра, и если вы прочитали одну из моих предыдущих статей , опубликованных на этом сайте, вы уже поняли, что это действительно так; мы можем создать приложение CLI за несколько минут :-).
Итак, я создал приложение CLI, которое обрабатывает сервер gRPC и клиент gRPC. Исходный код: https://github.com/scraly/hello-world .
Что касается управления зависимостями, я непосредственно вовлечен в использование модулей Go. Благодаря этому вам больше не придется иметь дело с ГОПАТОМ . Вы можете начать с клонирования репозитория, размещенного на GitHub (для версии Go <1.13 вы должны клонировать вне вашей GOPATH, если она у вас есть):
$ git clone https://github.com/scraly/hello-world.git
Первое, что нужно знать о нашем приложении, это то, что для облегчения жизни, для сборки, управления зависимостями, тестирования, генерации файлов и макетов, формата кода, выполнения статических тестов … мы используем magefile . Это «Make file», закодированный в Go, и это очень практично.
Вам также может понравиться:
10 советов по улучшению автоматизированного тестирования производительности внутри конвейеров CI (часть 2) .
Благодаря этому magefile нам не нужно выполнять все расширенные команды, которые позволят нам генерировать макеты, тестировать статические файлы, запускать модульные тесты и создавать наше приложение для генерации двоичного файла.
После клонирования репозитория Git я приглашаю вас выполнить следующую команду, которая позволит вам загрузить и установить необходимые инструменты:
$ go run mage.go -d tools
И это все! Нет необходимости покупать инструменты, размещенные в 50 репозиториях на GitHub или curl, и устанавливать их. С magefile вы получите все полезные двоичные файлы, которые позволят вам создавать, запускать линтеры, выполнять тестовые модули, проверять лицензии, генерировать макеты и т. Д.
Единственное, что нужно сделать сейчас, это сделать полную сборку нашего приложения:
Оболочка
x
1
$ go run mage.go
2
# Информация о сборке ----------------------------------------------- ----------------
4
Версия Go: go1.12
5
Git revision: f7ee9e3
6
Git филиал: мастер
7
Tag: 0 .0.1
8
# Основные пакеты ----------------------------------------------- -------------
10
## Вендорские зависимости
11
## Создать код
12
### Протобуф
13
#### Lint protobuf
14
## Форматируй всё
15
## Lint go code
16
## Запуск юнит-тестов
17
∅ cli / hello-world
19
∅ cli / hello-world / cmd
20
∅ cli / hello-world / config
21
∅ cli / hello-world / диспетчеры / grpc
22
∅ внутренние / услуги / pkg / v1
23
✓ внутренние / услуги / pkg / v1 / greeter (1.125 с)
24
∅ внутренний / версия
25
K pkg / protocol / helloworld / v1
26
СДЕЛАНО 0 тестов за 3 .425s
28
# Артефакты ------------------------------------------------ ---------------->
30
Создание hello-world [github.com/scraly/hello-world/cli/hello-world]
Большой! Все шаги прошли хорошо, и мы подготовили наш маленький бинарный файл!
$ ll bin total 39232 -rwxr-xr-x 1 uidn3817 CW01\Domain
Users 19M 7 aoû 18:55
hello-world
Давайте посмотрим на наше приложение. Что это действительно делает? Начнем с запуска нашего сервера gRPC:
$ bin/hello-world server grpc
Затем на другой вкладке вашего терминала запустите клиент gRPC для вызова нашего метода sayHello :
$ bin/hello-world client greeter sayHello -s 127.0.0.1:5555 <<<
'{"name": "me"}' { "message": "hello me" }%
Наше приложение работает правильно; он делает то, что нам хотелось: ответить « привет» + строковое поле, когда мы говорим «скажи привет ».
Но вынужден ли я создавать и заново генерировать все для запуска модульных тестов?
Нет, совсем нет, с помощью следующей команды мы можем легко выполнить наши модульные тесты:
xxxxxxxxxx
1
$ go run mage.go
2
перейти: тест
4
## Запуск юнит-тестов
5
∅ cli / hello-world (1мс)
6
∅ cli / hello-world / cmd
7
∅ cli / hello-world / config
8
∅ cli / hello-world / диспетчеры / grpc
9
∅ внутренние / услуги / pkg / v1
10
✓ внутренняя / услуги / pkg / v1 / greeter (1.092s)
11
∅ внутренний / версия
12
K pkg / protocol / helloworld / v1
13
Совершено 0 тестов в 3 .239s
Как видите, 0 модульных тестов были успешно выполнены. Мы рассмотрим их в следующем разделе, но перед этим необходимо знать, что стоит за этой целью (в нашем magefile.go ):go:test
// Test run go test func (Go) Test() error { color.Cyan("## Running unit tests") sh.Run("mkdir", "-p", "test-results/junit") return sh.RunV("gotestsum", "--junitfile", "test-results/junit/unit-tests.xml", "--", "-short", "-race", "-cover", "-coverprofile", "test-results/cover.out", "./...") }
Приведенный выше код показывает, что мы используем инструмент gotestsum для запуска наших модульных тестов и что результаты тестов экспортируются в формате JUnit в файл с именем test-results / junit / unit-tests.xml .
Итак, вы можете запустить тесты через magefile или с помощью gotestsum (если вы впервые установили утилиту на свой компьютер):
$ gotestsum --junitfile test-results/junit/unit-tests.xml -- -short -race
-cover -coverprofile test-results/cover.out ./…
Gotestsum
Gotestsum, что это за новый инструмент? Пройти тест недостаточно?
Давайте ответим на этот вопрос. Одним из преимуществ Go является его экосистема инструментов, которые позволяют нам сделать нашу жизнь проще . Чтобы проверить свой код, просто сделайте:
Оболочка
xxxxxxxxxx
1
$ go test ./…
2
? github.com/scraly/hello-world/cli/hello-world
3
[без тестовых файлов]
4
? github.com/scraly/hello-world/cli/hello-world/cmd
5
[без тестовых файлов]
6
? github.com/scraly/hello-world/cli/hello-world/config
7
[без тестовых файлов]
8
? github.com/scraly/hello-world/cli/hello-world/dispatchers/grpc
9
[без тестовых файлов]
10
? github.com/scraly/hello-world/internal/services/pkg/v1
11
[без тестовых файлов]
12
okgithub.com/scraly/hello-world/internal/services/pkg/v1/greeter0.037s
13
[нет тестов для запуска]
14
? github.com/scraly/hello-world/internal/version
15
[без тестовых файлов]
16
? github.com/scraly/hello-world/pkg/protocol/helloworld/v1
17
[без тестовых файлов]
Тестовый инструмент интегрирован с Go. Это удобно, но не очень удобно и интегрируется, например, во все решения CI / CD.
Вот почему gotestsum , небольшая утилита Go, предназначенная для запуска тестов с go test
улучшенным отображением результатов, делает более читабельный практический отчет с возможным выводом непосредственно в формате JUnit .
Как проверить gRPC
Наше приложение представляет собой клиент / сервер gRPC, так что это означает, что когда мы вызываем метод, соединение клиент / сервер инициируется, но нет никаких сомнений в том, чтобы проверять вызовы gRPC в наших модульных тестах. Мы sayHello
будем только проверять интеллект нашего приложения .
Наш сервер gRPC основан на файле protobuf с именем pkg / protocol / helloworld / v1 / greeter.proto :
Protobuf
xxxxxxxxxx
1
синтаксис = "proto3" ;
2
пакет helloworld . v1 ;
3
option csharp_namespace = "Helloworld.V1" ;
5
option go_package = "helloworldv1" ;
6
option java_multiple_files = true ;
7
option java_outer_classname = "GreeterProto" ;
8
option java_package = "com.scraly.helloworld.v1" ;
9
option objc_class_prefix = "HXX" ;
10
option php_namespace = "Helloworld \\ V1" ;
11
// Определение службы приветствия.
13
сервис Greeter {
14
// Отправляет приветствие.
15
RPC SayHello ( HelloRequest )
16
возвращает ( HelloReply ) {}
17
}
18
// Сообщение запроса, содержащее имя пользователя.
20
сообщение HelloRequest {
21
имя строки = 1 ;
22
}
23
// Ответное сообщение, содержащее приветствия.
24
сообщение HelloReply {
25
строковое сообщение = 1 ;
26
}
Из этого прото мы сгенерировали файлы .go благодаря gen protobuf
команде:
$ go run mage.go gen:protobuf
### Protobuf
#### Lint protobuf
Стандартная библиотека Go предоставляет нам пакет, который позволяет нам тестировать нашу программу Go . Тестовый файл в Go должен быть помещен в ту же папку, что и файл, который мы хотим протестировать, и должен иметь расширение _test.go . Необходимо придерживаться этого формализма, чтобы исполняемый файл Go распознавал наши тестовые файлы.
Первым шагом является создание файла service_test.go, который находится рядом с service.go .
Мы собираемся назвать пакет этого тестового файла greeter_tes t, и мы начнем с импорта пакета тестирования и создания функции, которую мы собираемся протестировать, которая выдает:
Идти
xxxxxxxxxx
1
пакет greeter_test
2
импорт ( «тестирование» )
3
func TestSayHello ( t * testing . T ) {
5
}
Предупреждение : каждая тестовая функция должна быть записана как funcTest***(t *testing.T)
, где « ***» представляет имя функции, которую мы хотим протестировать.
Давайте напишем тесты с помощью табличных тестов
В нашем приложении мы не будем тестировать все, но начнем с тестирования нашей бизнес-логики, интеллекта нашего приложения. В нашем приложении нас интересует то, что находится внутри service.go :
Привет мир
внутренний
Сервисы
упак
v1
привратник
service.go
Идти
xxxxxxxxxx
1
пакет привратник
2
импорт ( "контекст" ... )
3
структура службы типа {}
5
// Новый экземпляр сервиса
6
func New () apiv1 . Greeter {
8
возврат и обслуживание {}
9
}
10
// ------------------------------------------------ --------------------------
12
func ( s * service ) SayHello ( ctx context . Context , req
14
* helloworldv1 . HelloRequest ) ( * helloworldv1 . HelloReply , ошибка ) {
15
16
res : = & helloworldv1 . HelloReply {}
17
18
// Проверить запрос
19
если req == ноль {
20
войти . Bg () . Ошибка ( «запрос не должен быть нулем» )
21
вернуть Res , Xerrors . Errorf ( «запрос не должен быть нулевым» )
22
}
23
если REQ . Имя == "" {
25
войти . Bg () . Ошибка ( «имя, но не должно быть пустым в запросе» )
26
вернуть Res , Xerrors . Errorf ( «имя, но не должно быть пустым в запросе» )
27
}
28
рез . Сообщение = "привет" + REQ . название
30
вернуть Res , ноль
31
}
32
Как видите, чтобы покрыть максимальный объем нашего кода, нам нужно будет протестировать как минимум три случая:
- Запрос ноль.
- Запрос пуст (поле имени пусто).
- Имя поля заполняется в запросе.
Настольные тесты
Вместо того, чтобы создавать метод тестового примера и копировать и вставлять его, мы будем следовать настольным тестам , которые значительно облегчат жизнь.
Написание хороших тестов нелегко , но во многих ситуациях вы можете охватить множество вещей с помощью табличных тестов: каждая запись таблицы представляет собой полный тестовый набор с входными данными и ожидаемыми результатами. Иногда предоставляется дополнительная информация. Результаты теста легко читаются. Если вы обычно используете копирование и вставку при написании теста, спросите себя, может ли рефакторинг в табличном тесте быть лучшим вариантом.
Учитывая таблицу тестовых примеров, фактический тест просто сканирует все записи в таблице и выполняет необходимые тесты для каждой записи. Тестовый код записывается один раз и не рекомендуется для всех записей таблицы . Поэтому проще написать тщательный тест с хорошими сообщениями об ошибках.
Пример :
Я начну с определения моих тестовых случаев:
xxxxxxxxxx
1
testCases : = [] struct {
2
строка имени
3
req * helloworldv1 . HelloRequest
4
строка сообщения ожидаетсяErr bool
5
}
6
{
7
{
8
название : "req ok" ,
9
req : & helloworldv1 . HelloRequest {
10
Имя : "я"
11
},
12
сообщение : "привет мне" ,
13
Ожидаемая ошибка : ложь ,
14
},
15
{
16
имя : "запрос с пустым именем" ,
17
req : & helloworldv1 . HelloRequest {},
18
Ожидаемая ошибка : правда ,
19
},
20
{
21
название : "ноль запроса" ,
22
требование : ноль ,
23
Ожидаемая ошибка : правда ,
24
},
25
}
26
Хорошей практикой является предоставление имени для нашего тестового примера, поэтому, если во время его выполнения возникнет ошибка, будет написано имя тестового примера, и мы легко увидим, где находится наша ошибка.
Затем я перебираю все контрольные примеры. Я звоню в мою службу и, в зависимости от того, жду ли я ошибки, проверяю ее наличие, в противном случае проверяю, соответствует ли ожидаемый результат:
Идти
xxxxxxxxxx
1
для _ , tc : = range testCases {
2
testCase : = tc
3
т . Run ( TestCase . Имя , FUNC ( т * тестирование . T ) {
4
т . Параллельно ()
5
g : = NewGomegaWithT ( т )
6
Ctrl : = Гомок . NewController ( т )
7
отложить Ctrl . Готово ()
8
ctx : = context . Фон ()
9
10
// вызов
11
greeterSvc : = greeter . Новый ()
12
ответ , err : = greeterSvc . SayHello ( CTX , TestCase . REQ )
13
14
// утверждение результатов ожиданий
15
если testCase . pectedErr {
16
г . Ожидайте ( ответ ) . ToNot ( BeNil (), «Результат должен быть нулем» )
17
г . Ожидайте ( ошибаться ) . ToNot ( BeNil (), «Результат должен быть нулем» )
18
} еще {
19
г . Ожидайте ( ответ . Сообщение ) . Кому ( Равно ( testCase . Сообщение ))
20
}
21
})
22
}
23
Давайте практиковаться
Конкретно, чтобы добавить наш тестовый пример, мы будем:
- Создайте файл service_test.go рядом с его аналогом service.go, который мы хотим протестировать.
- Создать
TestSayHello
функцию .(t * testing.T)
- Определите наши тесты.
- Выполните цикл во всех тестовых случаях, позвоните в наш сервис и проверьте ошибку, если ожидается.
Вот что мы ожидаем:
xxxxxxxxxx
1
пакет greeter_test
2
импорт ( "контекст", "тестирование" . "github.com/onsi/gomega" "github.com/scraly/hello-world/internal/services/pkg/v1/greeter" helloworldv1 "github.com/scraly/hello-world / pkg / protocol / helloworld / v1 " )
3
func TestSayHello ( t * testing . T ) {
5
testCases : = [] struct {
6
строка имени
7
req * helloworldv1 . HelloRequest
8
строка сообщения
9
Ожидаемая ошибка
10
}
11
12
{
13
{
14
название : "req ok" ,
15
req : & helloworldv1 . HelloRequest {
16
Имя : "я"
17
},
18
сообщение : "привет мне" ,
19
Ожидаемая ошибка : ложь ,
20
},
21
{
22
имя : "запрос с пустым именем" ,
23
req : & helloworldv1 . HelloRequest {},
24
Ожидаемая ошибка : правда ,
25
},
26
{
27
название : "ноль запроса" ,
28
требование : ноль ,
29
Ожидаемая ошибка : правда ,
30
},
31
}
32
33
для _ , tc : = диапазон
34
testCases {
35
testCase : = tc
36
т . Run ( TestCase . Имя , FUNC ( т * тестирование . T ) {
37
т . Параллельно ()
38
g : = NewGomegaWithT ( т )
39
ctx : = context . Фон ()
40
41
// вызов
42
greeterSvc : = greeter . Новый ()
43
ответ , err : = greeterSvc . SayHello ( CTX , TestCase . REQ )
44
т . Журнал ( "Есть:" , ответ )
45
46
// утверждение результатов ожиданий
47
если testCase . pectedErr {
48
г . Ожидайте ( ответ ) . ToNot ( BeNil (), «Результат должен быть нулем» )
49
г . Ожидайте ( ошибаться ) . ToNot ( BeNil (), «Результат должен быть нулем» )
50
} еще {
51
г . Ожидайте ( ответ . Сообщение ) . Кому ( Равно ( testCase . Сообщение ))
52
}
53
}
54
)
55
}
56
}
Аурели, твой код хорош! Но зачем создавать новую переменную, testCase
, которая принимает значение, tc
, когда вы могли бы использовать tc
непосредственно?
Ответ в этой статье: https://gist.github.com/posener/92a55c4cd441fc5e5e85f27bca00872 .
Короче говоря, без этой строки, есть ошибка с хорошо известными сусликами - мы используем замыкание, которое является рутиной. Таким образом, вместо выполнения трех тестовых случаев: «req ok», «Req с пустым именем» и «nil request», будет три теста, но всегда со значениями первого теста :-(. t.Parallel()
И что такое гомега?
Gomega - это библиотека Go, которая позволяет вам делать утверждения. В нашем примере мы проверяем, является ли полученное значение нулевым, не нулевым или равным точному значению, но библиотека gomega намного богаче этого.
Для запуска вновь созданных модульных тестов, если вы используете VisualStudio Code , вы можете напрямую запустить их в своей IDE; это очень удобно
-
Открыть в service_test.go файл.
-
Затем нажмите на ссылку «запустить тестирование пакета»:
Код, выделенный зеленым, - это код, который охватывается тестами - супер, а не красная линия в поле зрения. Мы покрыли все!
В противном случае мы можем запустить все модульные тесты нашего проекта в командной строке благодаря нашему изумительному файлу magefile:
Оболочка
1
$ go run mage.go
2
перейти: тест
3
перейти: analyzecoverage
4
## Запуск юнит-тестов
5
∅cli / привет-мир
6
∅ cli / hello-world / cmd
7
∅ cli / hello-world / config
8
∅cli / привет-мир / диспетчеры / КПГР
9
∅ внутренние / услуги / pkg / v1
10
✓internal / services / pkg / v1 / greeter (1.089)
11
∅ внутренний / версия
12
K pkg / protocol / helloworld / v1
13
Совершено 4 испытания в 2 .973s
15
## Анализировать тестирование покрытия 2019/08/12 14:10:56
16
Анализ файлов тест-результаты / cover.out 2019 /08/12 14 : 10: 56
17
Бизнес - логика файл 2019 /08/12 14 : 10: 56
18
Минимальное пороговое значение покрытия Процент 90 +0,000000% 2019 /08/12 14 : 10: 56
19
Nb Заявления: 10
20
Процент покрытия: 100 .000000%
Круто, у нас есть 100% тестового покрытия нашей бизнес-логики!
Вывод
Если у вас есть привычка копировать вставку при написании тестовых случаев, я думаю, что вам придется серьезно взглянуть на тесты, управляемые таблицами, это действительно хорошая практика, которой нужно следовать при написании модульных тестов и, как мы видели, Написание юнит-тестов, которые охватывают наш код, становится детской игрой.