Наверняка я не открою ничего нового для большинства тех, кто давно использует 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 или ватева.