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