[Go] Изоляция вложенных вызовов в юнит-тестах

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.

Наверняка я не открою ничего нового для большинства тех, кто давно использует Go в работе. Но, зачастую оказывается, что люди не в курсе этого и мне будет проще отправлять их по ссылке, чем повторять из раза в раз одно и то же. Заодно может ещё кому-то будет полезно.

Дело вот в чём.

Допустим у нас есть структура с методами A, B, C. Но вот вдруг мы должны сделать вызов C из B, а ещё лучше, если появляется метод D и последовательность вызовов становится D->A + D->B->C в одном флаконе. В общем, – вложенные вызовы.

Если вложенные вызовы не изолировать, то тесты станут заметно длиннее и мы будем тестировать одно и то же в тестах разных методов.

Ситуация в коде:

package example

import (
	"github.com/google/uuid"
)

//go:generate mockgen -source example.go -destination gomocks/example.go -package gomocks

type Dependency interface {
	DoSomeWork(id uuid.UUID)
	DoAnotherWork(id uuid.UUID)
	DoAnotherWorkAgain(id uuid.UUID)
}

type X struct {
	dependency Dependency
}

func NewX(dependency Dependency) *X {
	return &X{dependency: dependency}
}

func (x *X) A(id uuid.UUID) {
	x.dependency.DoSomeWork(id)
}

func (x *X) B(id uuid.UUID) {
	x.dependency.DoAnotherWork(id)
	x.C(id)
}

func (x *X) C(id uuid.UUID) {
	x.dependency.DoAnotherWorkAgain(id)
}

func (x *X) D(id uuid.UUID) {
	x.A(id)
	x.B(id)
}

Обратите внимание на метод D. Он порождает длинные цепочки вызовов.

Теперь давайте представим, как может выглядеть тест метода D:

package example_test

import (
	"testing"

	"github.com/golang/mock/gomock"
	"github.com/google/uuid"
	"github.com/stretchr/testify/suite"

	"example"
	"example/gomocks"
)

func TestX(t *testing.T) {
	suite.Run(t, new(XTestSuite))
}

type XTestSuite struct {
	suite.Suite
	ctrl       *gomock.Controller
	dependency *gomocks.MockDependency
	x          *example.X
}

func (s *XTestSuite) SetupTest() {
	s.ctrl = gomock.NewController(s.T())
	s.dependency = gomocks.NewMockDependency(s.ctrl)
	s.x = example.NewX(s.dependency)
}

func (s *XTestSuite) TestD() {
	var id = uuid.MustParse("c73d6461-f461-4462-b1fe-0aa9b500f928")

	// Мы тестируем правильность работы не совсем тех методов, которые
	// мы тестируем сейчас, но и всех остальных методов. Мы прогоняем
	// всю логику насквозь. В ситуации, когда методы содержат десятки
	// вызовов и более-менее сложную логику, это становится похоже
	// на нетестируемый код из-за слишком высокой цикломатики.
	s.dependency.EXPECT().DoSomeWork(id)
	s.dependency.EXPECT().DoAnotherWork(id)
	s.dependency.EXPECT().DoAnotherWorkAgain(id)

	s.x.D(id)
}

Из этой ситуации есть простой выход. Что если изолировать X методы от самих себя?

Давайте добавим некоторые улучшения в наш код:

package example

import (
	"github.com/google/uuid"
)

//go:generate mockgen -source example.go -destination gomocks/example.go -package gomocks

type (
	Dependency interface {
		DoSomeWork(id uuid.UUID)
		DoAnotherWork(id uuid.UUID)
		DoAnotherWorkAgain(id uuid.UUID)
	}
	This interface {
		A(id uuid.UUID)
		B(id uuid.UUID)
	}
)

type X struct {
	dependency Dependency
	this       This
}

type Option func(x *X)

func WithThisMock(this This) Option {
	return func(x *X) {
		x.this = this
	}
}

func NewX(dependency Dependency, opts ...Option) *X {
	x := &X{dependency: dependency}

	for _, f := range opts {
		f(x)
	}

	if x.this == nil {
		x.this = x
	}

	return x
}

func (x *X) A(id uuid.UUID) {
	x.dependency.DoSomeWork(id)
}

func (x *X) B(id uuid.UUID) {
	x.dependency.DoAnotherWork(id)
	x.C(id)
}

func (x *X) C(id uuid.UUID) {
	x.dependency.DoAnotherWorkAgain(id)
}

func (x *X) D(id uuid.UUID) {
	// Изолировали вложенные вызовы.
	x.this.A(id)
	x.this.B(id)
}

Что мы тут сделали? Мы изолировали вызовы методов типа X из его же методов. Теперь мы можем написать тест метода D тестируя только логику метода D.

Смотрим на тест:

package example_test

import (
	"testing"

	"github.com/golang/mock/gomock"
	"github.com/google/uuid"
	"github.com/stretchr/testify/suite"

	"example"
	"example/gomocks"
)

func TestX(t *testing.T) {
	suite.Run(t, new(XTestSuite))
}

type XTestSuite struct {
	suite.Suite
	ctrl       *gomock.Controller
	dependency *gomocks.MockDependency
	this       *gomocks.MockThis
	x          *example.X
}

func (s *XTestSuite) SetupTest() {
	s.ctrl = gomock.NewController(s.T())
	s.dependency = gomocks.NewMockDependency(s.ctrl)
	s.this = gomocks.NewMockThis(s.ctrl)
	// В рабочем коде мы можем использовать
	// конструктор как example.NewX(realDependency).
	s.x = example.NewX(s.dependency, example.WithThisMock(s.this))
}

func (s *XTestSuite) TestD() {
	var id = uuid.MustParse("c73d6461-f461-4462-b1fe-0aa9b500f928")

	// Теперь мы тестируем только метод D.
	s.this.EXPECT().A(id)
	s.this.EXPECT().B(id)

	s.x.D(id)
}

Всё сильно упростилось, верно?

Надеюсь это будет полезно мне самому и мне больше не придётся повторять это на словах, а так же ещё кому-то, кто ещё не в теме. :)

По поводу самого слова this. Это наверно не совсем идиоматично, но можно использовать любое другое слово, например self или ватева.

Источник: https://habr.com/ru/post/664124/


Интересные статьи

Интересные статьи

В этой статье мы популярно объясняем на собственном опыте как организовать массовую выгрузку, обработку и загрузку фотографий товаров из Bitrix, используя Python и минимальное количество SQL. Для проч...
ВведениеВ данной статье я бы хотел рассмотреть проблему обновления PHP в виртуальной машине BitrixVM, и действия, которые возможно применить если выполнение переезда на машину с обновленным ПО невозмо...
В начале апреля на хабре была опубликована статья «JavaScript: Стек вызовов и магия его размера» — её автор пришёл к выводу, что каждый кадр стека занимает (72 + 8 * число_локальных_п...
Возможность интеграции с «1С» — это ключевое преимущество «1С-Битрикс» для всех, кто профессионально занимается продажами в интернете, особенно для масштабных интернет-магазинов.
Но если для интернет-магазина, разработанного 3–4 года назад «современные» ошибки вполне простительны потому что перед разработчиками «в те далекие времена» не стояло таких задач, то в магазинах, сдел...