golang

Пишем gRPC автотесты на Go с Allure отчетом

  • воскресенье, 11 июня 2023 г. в 00:00:13
https://habr.com/ru/articles/736502/

Вступление

В данной статье разберем, как писать 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 {}

В контракте описаны методы:

Также, обратите внимание, что есть модель 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

Полную версию отчета посмотрите тут.

Заключение

Весь исходный код проекта с автотестами расположен на моем github.

Весь исходный код проекта с сервером расположен на моем github.