Вступление
В данной статье разберем, как писать gRPC автотесты с использованием языка Go, а также сделаем Allure отчет.
Перед тем, как читать статью, нужно базово понимать некоторые термины:
Что такое RPC?
Что такое gRPC?
Что такое protobuf? Сюда же можно отнести знакомство с синтаксисом *.proto файлов;
Неплохо было бы знать/понимать синтаксис языка Go, хотя бы на базовом уровне;
Для запуска сервера через docker понадобятся базовые знания docker.
Без понимания вышеописанного будет сложно разобраться о чем идет речь.
Requirements
Для написания автотестов нам понадобится gRPC сервер. Поискал на просторах интернета открытые gRPC сервера, но ничего нашел. Поэтому придется написать свой сервер, который можно будет запустить локально и уже на него писать автотесты.
Исходный код сервера расположен на моем github. Инструкции по установке и настройке сервера прилагаются. Если у вас нет необходимости как-то изменять/расширять имеющийся контракт, то можно сразу пропустить секцию Setup protobuf.
Запустить сервер можно двумя способами:
По этой инструкции установить все необходимые зависимости на вашу OS и запустить сервер;
Либо запустить сервер внутри docker, но тогда нужно посмотреть эту инструкцию.
После запуска сервера локально он будет доступен по адресу localhost:8000 или же 127.0.0.1:8000. До написания автотестов неплохо было бы иметь клиент, с помощью которого можно будет "потыкать" сервер. Таким клиентом может быть Postman, но только десктопный вариант, тк в браузере gRPC не может быть использован. Инструкции о том, как настроить Postman на работу c gRPC.
Теперь посмотрим на структуру контракта, который находится в репозитории с сервером. Этот же контракт мы будем использовать в автотестах:
syntax = "proto3";
option go_package = "./;articlesservice";
service ArticlesService {
rpc GetArticle (GetArticleRequest) returns (GetArticleResponse);
rpc CreateArticle(CreateArticleRequest) returns (CreateArticleResponse);
rpc UpdateArticle(UpdateArticleRequest) returns (UpdateArticleResponse);
rpc DeleteArticle(DeleteArticleRequest) returns (DeleteArticleResponse);
}
message Article {
string id = 1;
string title = 2;
string author = 3;
string description = 4;
}
enum ErrorType {
NOT_FOUND = 0;
ALREADY_EXISTS = 1;
UNSPECIFIED = 2;
}
message Error {
string message = 1;
ErrorType type = 2;
}
message GetArticleRequest {
string article_id = 1;
}
message GetArticleResponse {
oneof result {
Error error = 1;
Article article = 2;
}
}
message CreateArticleRequest {
Article article = 1;
}
message CreateArticleResponse {
oneof result {
Error error = 1;
Article article = 2;
}
}
message UpdateArticleRequest {
Article article = 1;
}
message UpdateArticleResponse {
oneof result {
Error error = 1;
Article article = 2;
}
}
message DeleteArticleRequest {
string article_id = 1;
}
message DeleteArticleResponse {}
В контракте описаны методы:
GetArticle - получение статьи по id. Запрос GetArticleRequest и ответ GetArticleResponse;
CreateArticle - создание статьи. Запрос CreateArticleRequest и ответ CreateArticleResponse;
UpdateArticle - изменение статьи. Запрос UpdateArticleRequest и ответ UpdateArticleResponse;
DeleteArticle - удаление статьи. Запрос DeleteArticleRequest и ответ DeleteArticleResponse;
Также, обратите внимание, что есть модель Article, которая используется в методах создания, обновления и получения статьи. Эта модель специально вынесена отдельно, чтобы лишний раз не дублировать код.
Выше приведен очень простой контракт на стандартные CRUD операции. На реальных проектах контракты могут быть намного сложнее, но для тестов нам подойдет и такой.
Сторонние библиотеки, которые понадобятся:
grpc-go - для реализации gRPC клиента;
allure-go - для Allure отчета;
yaml - для чтения yaml файлов;
gomega - для проверок;
zap - для логирования;
dig - для реализации dependency injection;
sample_go_grpc_server - это сервер, про который говорилось выше, нужен для контрактов.
Есть еще ряд библиотек без которых никуда, но все они уже встроены в go.
Configuration
Перед написанием автотестов нам необходимо добавить файл с конфигурациями, в котором опишем основные настройки
infrastructure/config-local.yml
articlesService:
port: 8000
host: localhost
logger:
isDevMode: true
level: debug
Напишем настройки для сервиса статей и логгера; скорее всего, для вашего проекта понадобится больше конфигов, поэтому можете добавить их в этот infrastructure/config-local.yml файл.
Лайфхак. Если есть необходимость запускать автотесты сразу на нескольких окружениях, то для этого можно создать файлы настроек под каждое из окружений, как, например, в нашем случае:
config-dev.yml
config-local.yml
config-stable.yml
Далее, в зависимости от переменной окружения ENV, будем брать нужный файл и загружать настройки. Подробнее разберем ниже, когда будем описывать чтение настроек в файле utils/config/internal.go
Напишем модели, внутрь которых будут "помещаться" конфиги из файла infrastructure/config-{env}.yml
utils/config/models.go
package config
type Env string
type LogLevel string
const (
DebugLogLevel LogLevel = "debug"
InfoLogLevel LogLevel = "info"
WarningLogLevel LogLevel = "warning"
ErrorLogLevel LogLevel = "error"
)
type Logger struct {
Level LogLevel `yaml:"level"`
IsDevMode bool `yaml:"isDevMode"`
}
type GrpcService struct {
Port int32 `yaml:"port"`
Host string `yaml:"host"`
InsecureSkipVerify bool `yaml:"insecureSkipVerify"`
}
type Config struct {
Logger Logger `yaml:"logger" validate:"required"`
Articlesservice GrpcService `yaml:"articlesService" validate:"required"`
}
Теперь напишем парсер, который будет читать конфиги и "раскладывать" их по моделям из utils/config/models.go
utils/config/internal.go
package config
import (
"fmt"
"gopkg.in/yaml.v3"
"io/ioutil"
"os"
)
func getEnv() (Env, error) {
env := os.Getenv("ENV")
if len(env) == 0 {
return "", fmt.Errorf("cannot parse env variable")
}
return Env(env), nil
}
func readConfig(configPath string, config interface{}) error {
if configPath == `` {
return fmt.Errorf(`no config path`)
}
configBytes, err := ioutil.ReadFile(configPath)
if err != nil {
return err
}
if err = yaml.Unmarshal(configBytes, config); err != nil {
return err
}
return nil
}
func NewConfig() (Config, error) {
var config Config
env, err := getEnv()
if err != nil {
return Config{}, err
}
err = readConfig(fmt.Sprintf("../infrastructure/config-%s.yml", env), &config)
if err != nil {
return Config{}, err
}
return config, nil
}
Обратите внимание, что мы динамически подставляем окружение в путь до настроек "../infrastructure/config-%s.yml"
Service
Добавим методы для взаимодействия с нашим сервером, но перед этим необходимо сделать gRPC клиент
utils/services/grpc/client.go
package grpc
import (
"fmt"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
"sample_go_grpc_testing/utils/config"
)
func GetGrpcClient(grpcService config.GrpcService) (*grpc.ClientConn, error) {
address := fmt.Sprintf("%s:%d", grpcService.Host, grpcService.Port)
return grpc.Dial(address, grpc.WithTransportCredentials(insecure.NewCredentials()))
}
Функция GetGrpcClient
будет принимать настройки Host
, Port
для инициализации клиента, создавать клиент и возвращать его
Теперь можно написать клиент для сервиса ArticlesService. Структура, которой будем придерживаться:
client.go - описываем клиент и билдер, который будет конструировать клиент;
api.go - будет описывать методы для взаимодействия с сервером. По сути, это будет просто обертка, внутри которой накинем логи, проверки;
steps.go - добавляем к методам из api.go allure шаги, описание, параметры и всё, что связано с отчетом. Можете пропустить этот слой, если вам не нужны шаги отчета либо сам отчет;
core/articlesservice/client.go
package articlesservice
import (
articlesservice "github.com/Nikita-Filonov/sample_go_grpc_server/gen/proto"
"sample_go_grpc_testing/utils/config"
"sample_go_grpc_testing/utils/logger"
"sample_go_grpc_testing/utils/services/grpc"
)
type Client struct {
articlesservice.ArticlesServiceClient
logger *logger.CtxLogger
}
func NewClient(logger logger.Service, conf config.Config) (*Client, error) {
conn, err := grpc.GetGrpcClient(conf.Articlesservice)
if err != nil {
return &Client{}, err
}
return &Client{
logger: logger.NewPrefix("ARTICLES_SERVICE.GRPC.CLIENT"),
ArticlesServiceClient: articlesservice.NewArticlesServiceClient(conn),
}, nil
}
core/articlesservice/api.go
package articlesservice
import (
"context"
articlesservice "github.com/Nikita-Filonov/sample_go_grpc_server/gen/proto"
"github.com/onsi/gomega"
)
func (c *Client) getArticle(
g gomega.Gomega,
ctx context.Context,
request *articlesservice.GetArticleRequest,
) *articlesservice.GetArticleResponse {
c.logger.InfofJSON("GetArticleRequest", request)
res, err := c.ArticlesServiceClient.GetArticle(ctx, request)
g.Expect(err).ShouldNot(gomega.HaveOccurred(), "GetArticle error")
c.logger.InfofJSON("GetArticleResponse", res)
return res
}
func (c *Client) createArticle(
g gomega.Gomega,
ctx context.Context,
request *articlesservice.CreateArticleRequest,
) *articlesservice.CreateArticleResponse {
c.logger.InfofJSON("CreateArticleRequest", request)
res, err := c.ArticlesServiceClient.CreateArticle(ctx, request)
g.Expect(err).ShouldNot(gomega.HaveOccurred(), "CreateArticle error")
c.logger.InfofJSON("CreateArticleResponse", res)
return res
}
func (c *Client) updateArticle(
g gomega.Gomega,
ctx context.Context,
request *articlesservice.UpdateArticleRequest,
) *articlesservice.UpdateArticleResponse {
c.logger.InfofJSON("UpdateArticleRequest", request)
res, err := c.ArticlesServiceClient.UpdateArticle(ctx, request)
g.Expect(err).ShouldNot(gomega.HaveOccurred(), "UpdateArticle error")
c.logger.InfofJSON("UpdateArticleResponse", res)
return res
}
func (c *Client) deleteArticle(
g gomega.Gomega,
ctx context.Context,
request *articlesservice.DeleteArticleRequest,
) *articlesservice.DeleteArticleResponse {
c.logger.InfofJSON("DeleteArticleRequest", request)
res, err := c.ArticlesServiceClient.DeleteArticle(ctx, request)
g.Expect(err).ShouldNot(gomega.HaveOccurred(), "DeleteArticle error")
c.logger.InfofJSON("DeleteArticleResponse", res)
return res
}
Из чего состоит каждый метод:
Логирование запроса, который отправляется на сервер;
Вызов удаленной процедуры;
Обработка ошибки с помощью gomega. В случае, если сервер ответил ошибкой, то тест упадет уже на этом этапе. Если вам наоборот нужно вызвать ошибку, то можно написать другой метод и добавить проверку на то, что была получена ошибка
g.Expect(err).Should(gomega.HaveOccurred(), "GetArticle error")
;Если все хорошо и ошибки не случилось, то логируем ответ от сервера.
core/articlesservice/steps.go
package articlesservice
import (
"context"
articlesservice "github.com/Nikita-Filonov/sample_go_grpc_server/gen/proto"
"github.com/dailymotion/allure-go"
"github.com/onsi/gomega"
)
func (c *Client) GetArticle(
g gomega.Gomega,
request *articlesservice.GetArticleRequest,
) (response *articlesservice.GetArticleResponse) {
allure.Step(allure.Description("Send ArticlesService.GetArticle request"), allure.Action(func() {
response = c.getArticle(g, context.Background(), request)
}))
return response
}
func (c *Client) CreateArticle(
g gomega.Gomega,
request *articlesservice.CreateArticleRequest,
) (response *articlesservice.CreateArticleResponse) {
allure.Step(allure.Description("Send ArticlesService.CreateArticle request"), allure.Action(func() {
response = c.createArticle(g, context.Background(), request)
}))
return response
}
func (c *Client) UpdateArticle(
g gomega.Gomega,
request *articlesservice.UpdateArticleRequest,
) (response *articlesservice.UpdateArticleResponse) {
allure.Step(allure.Description("Send ArticlesService.UpdateArticle request"), allure.Action(func() {
response = c.updateArticle(g, context.Background(), request)
}))
return response
}
func (c *Client) DeleteArticle(
g gomega.Gomega,
request *articlesservice.DeleteArticleRequest,
) (response *articlesservice.DeleteArticleResponse) {
allure.Step(allure.Description("Send ArticlesService.DeleteArticle request"), allure.Action(func() {
response = c.deleteArticle(g, context.Background(), request)
}))
return response
}
Отлично, шаги написали, теперь стоит добавить утилиты, которые понадобятся для написания тестов.
Logger
Логирование - очень важный атрибут и без него будет сложно понять, что сейчас делает тест, на каком именно шаге он упал, что ответил сервер.
В качестве библиотеки для логирования будем использовать zap
Напишем конфиги для логгера и билдер
package logger
import (
"sample_go_grpc_testing/utils/config"
"time"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
)
var MapConfLevelZapLevel = map[config.LogLevel]zapcore.Level{
config.DebugLogLevel: zap.DebugLevel,
config.InfoLogLevel: zap.InfoLevel,
config.WarningLogLevel: zap.WarnLevel,
config.ErrorLogLevel: zap.ErrorLevel,
}
type Service interface {
NewPrefix(prefix string) *CtxLogger
}
type loggerService struct {
*zap.Logger
}
func (ls *loggerService) NewPrefix(prefix string) *CtxLogger {
return &CtxLogger{ls.Logger.Named(prefix)}
}
func NewLoggerService(conf config.Config) (Service, error) {
cfg := newConfig(conf.Logger)
zapLogger, err := cfg.Build()
if err != nil {
return nil, err
}
return &loggerService{zapLogger}, err
}
func utcTimeEncoder(t time.Time, enc zapcore.PrimitiveArrayEncoder) {
enc.AppendString(t.UTC().Format("2006-01-02T15:04:05.000Z0700"))
}
func newEncoderConfig() zapcore.EncoderConfig {
return zapcore.EncoderConfig{
TimeKey: "@timestamp",
LevelKey: "level",
NameKey: "logger_name",
CallerKey: "caller_file",
MessageKey: "message",
StacktraceKey: "stacktrace",
LineEnding: zapcore.DefaultLineEnding,
EncodeLevel: zapcore.CapitalLevelEncoder,
EncodeTime: utcTimeEncoder,
EncodeDuration: zapcore.StringDurationEncoder,
EncodeCaller: zapcore.ShortCallerEncoder,
}
}
func newConfig(conf config.Logger) zap.Config {
cfg := zap.Config{
Level: zap.NewAtomicLevelAt(MapConfLevelZapLevel[conf.Level]),
Development: false,
DisableCaller: false,
DisableStacktrace: false,
Sampling: &zap.SamplingConfig{
Initial: 100,
Thereafter: 100,
},
Encoding: "json",
EncoderConfig: newEncoderConfig(),
OutputPaths: []string{"stdout"},
ErrorOutputPaths: []string{"stdout"},
}
if conf.IsDevMode {
cfg.Development = true
cfg.Encoding = "console"
}
return cfg
}
utcTimeEncoder
,newEncoderConfig
,newConfig
- по сути это просто утилиты, которые помогают настроить логгер;NewLoggerService - будет создавать и настраивать сервис логгера;
NewPrefix - будет использоваться для добавления логгера к конкретному сервису, как мы это делали чуть ранее.
Components/dependency injection
Будем использовать библиотеку dig для реализации dependency injection.
Что такое dependency injection? Про это подробнее можно почитать, например, тут или тут.
Зачем нам это нужно? Давайте разберем на примере автомобиля. Автомобиль состоит из колес, руля, двигателя, электроники, бензобака и т.д. Но конечного клиента, то есть водителя, не волнует, как, где, кем и когда были добавлены/созданы эти детали, водителю нужен лишь интерфейс управления машиной/двигателем. Водителю важен лишь конечный результат - в машину можно сесть, завести и поехать. Аналогично и с автотестами, - для них требуется много сервисов/клиентов, например, базы данных, внутренние или внешние сервисы, рестовые апишки, вольт с кредами и т.д. По сути говоря, внутри теста не важно, кто, где и как инициализировал этих клиентов, нужен лишь интерфейс, чтобы мы могли пойти в базу данных, отправить запрос на сервер, получить какие-то креды и т.д.
Опишем компоненты, которые потом будем использовать в тестах:
utils/common/container/components.go
package container
import (
"go.uber.org/dig"
"sample_go_grpc_testing/core/articlesservice"
"sample_go_grpc_testing/utils/config"
"sample_go_grpc_testing/utils/logger"
)
type Components struct {
ArticlesService *articlesservice.Client
Logger logger.Service
Config config.Config
}
func initComponents(c *dig.Container) (*Components, error) {
var err error
components := Components{}
err = c.Invoke(func(
articlesService *articlesservice.Client,
logger logger.Service,
conf config.Config,
) {
components.Config = conf
components.Logger = logger
components.ArticlesService = articlesService
})
if err != nil {
return nil, err
}
return &components, nil
}
Теперь напишем билдер, который будет инициализировать компоненты:
utils/common/container/api.go
package container
import (
"go.uber.org/dig"
"sample_go_grpc_testing/core/articlesservice"
"sample_go_grpc_testing/utils/config"
"sample_go_grpc_testing/utils/logger"
)
func BuildContainer() (*Components, error) {
c := dig.New()
servicesConstructors := []interface{}{
articlesservice.NewClient,
config.NewConfig,
logger.NewLoggerService,
}
for _, service := range servicesConstructors {
err := c.Provide(service)
if err != nil {
return nil, err
}
}
components, componentsError := initComponents(c)
if componentsError != nil {
return nil, componentsError
}
return components, nil
}
Подробнее про использование библиотеки dig вы можете почитать тут
Assertions
Для проверок будем использовать библиотеку gomega. Полную документацию можно почитать тут, лишь кратко скажу, что данная библиотека закрывает все базовые потребности для проверок:
Проверить, что значения равны/не равны;
Асинхронные проверки, например, нужно подождать, когда сервис вернет нужный статус код.
У gomega еще есть много крутых фичей и настроек, но нам будет достаточно вышеперечисленного.
Для начала напишем gomega инициализатор, который будет инициализировать gomega и добавлять базовые настройки:
utils/common/gomega.go
package common
import (
"github.com/dailymotion/allure-go"
"github.com/onsi/gomega"
"github.com/pkg/errors"
"runtime/debug"
"testing"
"time"
)
func GetGomega(t *testing.T) gomega.Gomega {
g := gomega.NewWithT(t)
g.SetDefaultEventuallyTimeout(time.Second * 20)
g.SetDefaultEventuallyPollingInterval(time.Second * 3)
g.ConfigureWithFailHandler(func(message string, callerSkip ...int) {
g.THelper()
allure.Fail(errors.New(message))
t.Fatalf("\n%s %s", message, debug.Stack())
})
g.THelper = t.Helper
return g
}
Теперь нужно описать базовые проверки, которые будут использоваться во всем проекте:
utils/assertions/common/solutions.go
package solutions
import (
"fmt"
"github.com/dailymotion/allure-go"
"github.com/onsi/gomega"
)
func AssertToEqual(g gomega.Gomega, actual interface{}, expected interface{}, description string) {
step := fmt.Sprintf("Checking that '%s' equals to '%s'", description, PrettifyValue(expected))
allure.Step(allure.Description(step), allure.Action(func() {
g.Expect(actual).To(gomega.Equal(expected), step)
}))
}
Нам понадобится только одна проверка AssertToEqual, но в вашем проекте, скорее всего, понадобится намного больше проверок по типу AssertIsNotNil, AssertIsNil, AssertToHaveLen и т.д. Их также можно будет объявить в этом файле и далее использовать по всему проекту.
Лайфхак. Обратите внимание на то, каким образом значение expected
подставляется в шаблон шага "Checking that '%s' equals to '%s'"
. Функция PrettifyValue специально используется, чтобы придать значению expected
читабельный вид. Если использовать общие проверки для разных типов данных, то такой "Checking that '%s' equals to '%s'"
шаблон форматирования будет нормально работать только для строк. Также в go есть поинторы, которые тоже будут отображаться некорректно. Конечно же, вы можете написать проверку под каждый тип, например, AssertToEqualString, AssertToEqualInt, AssertToEqualFloat и т.д., но тогда придется писать очень много дублированных проверок и под каждую нужно будет делать свой шаблон. В нашем случае функция PrettifyValue получает на вход любое значение и возвращает уже отформатированный, человеко-читабельный вариант в формате type[value]
, например, string[MyString], int8[4], int32[1234] и т.д. Вы можете использовать другое решение, это лишь пример, как можно, не дублируя проверки, сделать красивые шаги в отчете.
Теперь напишем конкретные проверки уже для сервиса ArticlesService, а именно для модели Article
utils/assertions/articlesservice/articles.go
package articlesservicechecks
import (
"fmt"
articlesservice "github.com/Nikita-Filonov/sample_go_grpc_server/gen/proto"
"github.com/dailymotion/allure-go"
"github.com/onsi/gomega"
solutions "sample_go_grpc_testing/utils/assertions/common"
)
func CheckArticle(g gomega.Gomega, actualArticle, expectedArticle *articlesservice.Article) {
allure.Step(allure.Description("Checking article"), allure.Action(func() {
solutions.AssertToEqual(g, actualArticle.Id, expectedArticle.Id, "Article Id")
solutions.AssertToEqual(g, actualArticle.Title, expectedArticle.Title, "Article Title")
solutions.AssertToEqual(g, actualArticle.Author, expectedArticle.Author, "Article Author")
solutions.AssertToEqual(g, actualArticle.Description, expectedArticle.Description, "Article Description")
}))
}
func CheckArticleError(g gomega.Gomega, actualError, expectedError *articlesservice.Error) {
allure.Step(allure.Description("Checking article error"), allure.Action(func() {
solutions.AssertToEqual(g, actualError.Type, expectedError.Type, "Error Type")
solutions.AssertToEqual(g, actualError.Message, expectedError.Message, "Error Message")
}))
}
func CheckArticleNotFoundError(g gomega.Gomega, actualError *articlesservice.Error, articleId string) {
expectedError := articlesservice.Error{
Type: articlesservice.ErrorType_NOT_FOUND,
Message: fmt.Sprintf("Article with Id %s not found", articleId),
}
CheckArticleError(g, actualError, &expectedError)
}
Лайфхак. Если вы пишете тесты для gRPC сервиса, то у вас наверняка есть много похожих моделей в проекте, которые используются в разных сервисах или могут быть вложены в другие модели. Для общих моделей сразу стоит делать отдельные проверки, например, есть модель User, тогда делаем проверку CheckUser и используем ее внутри других проверок, например, так:
...
func CheckAccount(g gomega.Gomega, actualAccount, expectedAccount *accountservice.Account) {
allure.Step(allure.Description("Checking account"), allure.Action(func() {
...
CheckUser(g, actualAccount.User, expectedAccount.User)
...
}))
}
...
Это сэкономит больше времени в будущем при написании новых проверок или же, если нужно будет вносить изменения в уже имеющиеся проверки.
Utils
Теперь почти все готово к написанию автотестов, добавим лишь несколько утилит, которые помогут в написании тестов.
Напишем функцию GetRandomArticle
, которая будет возвращать модель Article заполненную рандомными данными. Реализацию функции RandomString
можно посмотреть тут.
utils/controllers/articlesservice/articles.go
package articlesservicecontrollers
import (
articlesservice "github.com/Nikita-Filonov/sample_go_grpc_server/gen/proto"
"github.com/google/uuid"
"sample_go_grpc_testing/utils/fakers"
)
func GetRandomArticle() *articlesservice.Article {
return &articlesservice.Article{
Id: uuid.NewString(),
Title: fakers.RandomString(30),
Author: fakers.RandomString(20),
Description: fakers.RandomString(100),
}
}
Добавим функцию, которая будет подготавливать нужные для теста компоненты; в нашем случае components, gomega.
utils/common/setup.go
package common
import (
"fmt"
"github.com/onsi/gomega"
"go.uber.org/dig"
"sample_go_grpc_testing/utils/common/container"
"testing"
)
func SetupTesting(t *testing.T) (*container.Components, gomega.Gomega) {
g := GetGomega(t)
c, err := container.BuildContainer()
g.Expect(err).ShouldNot(gomega.HaveOccurred(), fmt.Sprintf("unable to build container: %v", dig.RootCause(err)))
return c, g
}
Также вынесем все части касающиеся отчета в отдельный пакет, чтобы потом их можно было использовать в других автотестах.
Testing
Теперь можно писать автотесты. Всего у сервиса ArticlesService, четыре метода, значит напишем четыре простых позитивных теста
tests/articles_test.go
package tests
import (
articlesservice "github.com/Nikita-Filonov/sample_go_grpc_server/gen/proto"
"github.com/dailymotion/allure-go"
"github.com/dailymotion/allure-go/severity"
articlesservicechecks "sample_go_grpc_testing/utils/assertions/articlesservice"
"sample_go_grpc_testing/utils/common"
articlesservicecontrollers "sample_go_grpc_testing/utils/controllers/articlesservice"
"sample_go_grpc_testing/utils/reports"
"testing"
)
func TestGetArticle(t *testing.T) {
t.Parallel()
allure.Test(t, reports.ArticlesServiceFeature, reports.ArticlesSuite,
allure.Severity(severity.Critical),
allure.Tags(reports.ArticlesTag),
allure.Name("Get article"),
allure.Action(func() {
c, g := common.SetupTesting(t)
article := articlesservicecontrollers.GetRandomArticle()
c.ArticlesService.CreateArticle(g, &articlesservice.CreateArticleRequest{Article: article})
response := c.ArticlesService.GetArticle(g, &articlesservice.GetArticleRequest{ArticleId: article.Id})
articlesservicechecks.CheckArticle(g, response.GetArticle(), article)
}),
)
}
func TestCreateArticle(t *testing.T) {
t.Parallel()
allure.Test(t, reports.ArticlesServiceFeature, reports.ArticlesSuite,
allure.Severity(severity.Critical),
allure.Tags(reports.ArticlesTag),
allure.Name("Create article"),
allure.Action(func() {
c, g := common.SetupTesting(t)
article := articlesservicecontrollers.GetRandomArticle()
response := c.ArticlesService.CreateArticle(g, &articlesservice.CreateArticleRequest{Article: article})
articlesservicechecks.CheckArticle(g, response.GetArticle(), article)
}),
)
}
func TestUpdateArticle(t *testing.T) {
t.Parallel()
allure.Test(t, reports.ArticlesServiceFeature, reports.ArticlesSuite,
allure.Severity(severity.Critical),
allure.Tags(reports.ArticlesTag),
allure.Name("Update article"),
allure.Action(func() {
c, g := common.SetupTesting(t)
article := articlesservicecontrollers.GetRandomArticle()
c.ArticlesService.CreateArticle(g, &articlesservice.CreateArticleRequest{Article: article})
response := c.ArticlesService.UpdateArticle(g, &articlesservice.UpdateArticleRequest{Article: article})
articlesservicechecks.CheckArticle(g, response.GetArticle(), article)
}),
)
}
func TestDeleteArticle(t *testing.T) {
t.Parallel()
allure.Test(t, reports.ArticlesServiceFeature, reports.ArticlesSuite,
allure.Severity(severity.Critical),
allure.Tags(reports.ArticlesTag),
allure.Name("Delete article"),
allure.Action(func() {
c, g := common.SetupTesting(t)
article := articlesservicecontrollers.GetRandomArticle()
c.ArticlesService.CreateArticle(g, &articlesservice.CreateArticleRequest{Article: article})
c.ArticlesService.DeleteArticle(g, &articlesservice.DeleteArticleRequest{ArticleId: article.Id})
response := c.ArticlesService.GetArticle(g, &articlesservice.GetArticleRequest{ArticleId: article.Id})
articlesservicechecks.CheckArticleNotFoundError(g, response.GetError(), article.Id)
}),
)
}
Report
Перед запуском тестов убедитесь, что сервер запущен и работает
Запустим тесты и посмотрим на отчет:
make test
Теперь запустим отчет:
allure serve
Либо можете собрать отчет и в папке allure-reports открыть файл index.html:
allure generate
![](https://habrastorage.org/getpro/habr/upload_files/676/836/8a8/6768368a85389f4c25bc369c2f9328ab.png)
Полную версию отчета посмотрите тут.
Заключение
Весь исходный код проекта с автотестами расположен на моем github.
Весь исходный код проекта с сервером расположен на моем github.