[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/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *