Важной частью создания програмного обеспечения является покрытие кодовой базы тестами.
В основании всем известной пирамиды тестрования лежат юнит тесты. Не будем с ней спорить, а будем писать юниты, много юнит тестов. Не всегда можно просто взять и написать тест, иногда для тестирования нам нужно заглушить какие-то зависимости, будь то доступ в базу данных, сетевое взаимодействие или какая-нибудь магия.
Естественно, для этого придумали "моки". Придумали не только их, но в данной статье речь пойдет только про моки и немного про кодогенерацию, не писать же их руками.
У нас есть некоторый оъект, у которого есть внешняя зависимость. Внешнюю зависимость мы подсовывем при инстанцировании, не вызываем её непонятно откуда, как минимум, это не красиво. Также мы используем интерфейс для внешней зависимости.
type Action struct { s interfaces.ServiceInterface } func NewAction(s interfaces.ServiceInterface) Action { return Action{s: s} } func (a Action) Make(s string) int { i, err := a.s.SomeAction(s) if err != nil { return 1 } return i }
Интерфейс очень простой. В нём один метод, который делает что-то.
type ServiceInterface interface { SomeAction(string) (int, error) }
Зачем он нужен? Как минимум писать интерфейсы - это хорошо. Но, в данном случае, на его основе мы будем генерировать mock.
s, который соответствует ServiceInterface, может быть чем угодно. Он может ходить в базу, обращаться по сети в другой сервис. Для примера сделаем простую реализацию, которая не делает практически ничего.
type Service struct{} func (s Service) SomeAction(str string) (int, error) { if str == "a" { return 100, nil } return 0, errors.New("error") }
Все готово, можно тестировать. По-хорошему бы начать с тестов, но главное чтобы они вообще были, так что приступим.
func TestMain(t *testing.T) { tests := []struct { name string service interfaces.ServiceInterface inputString string want int }{ { name: "test with a input", service: internal.Service{}, inputString: "a", want: 100, }, { name: "test with s input", service: internal.Service{}, inputString: "s", want: 1, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { s := internal.NewAction(tt.service) got := s.Make(tt.inputString) if tt.want != got { t.Errorf("Action() got = %v, want %v", got, tt.want) } }) } }
Тесты проходят. Посмотрев на код, мы видим, что поведение зависит от internal.Service{}. Нашей внешней зависимости. Зависит достаточно прямолинейно, но это код только для статьи.
Возможно, что реализация былабы куда сложнее и тянула за собой другие зависимости.
Уберем инстанс internal.Service{} из тестов, заменим на другую реализацию ServiceInterface. Для этого будем использовать пакет github.com/golang/mock/gomock. Делается это довольно просто.
Так как go умеет писать код за нас, пусть пишет. Дописываем к нашему интерфейсу строку для go generate и получаем следующий код:
//go:generate mockgen -source=service.go -destination=../internal/mock/service.go -package=mock type ServiceInterface interface { SomeAction(string) (int, error) }
Где все должно быть понятно. У нас есть файл-источник, путь куда мы положим результат кодогенерации и имя пакета, который мы сгенерируем.
Так же нам нужно поставить mockgen, сделать это тоже просто.
go install github.com/golang/mock/mockgen
Запустим кодогенерацию.
go generate interfaces/service.go
B всё готово.
Модифицируем наши тесты. Напишем функцию для создания мока сервиса simulateService, чтобы на лету менять ожидания от него.
func TestMain(t *testing.T) { controller := gomock.NewController(t) tests := []struct { name string service interfaces.ServiceInterface inputString string want int }{ { name: "test with error but with a", service: simulateService(controller, 100, errors.New("error")), inputString: "a", want: 1, }, { name: "test without error", service: simulateService(controller, 500, nil), inputString: "s", want: 500, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { s := internal.NewAction(tt.service) got := s.Make(tt.inputString) if tt.want != got { t.Errorf("Action() got = %v, want %v", got, tt.want) } }) } } func simulateService(controller *gomock.Controller, res int, err error) interfaces.ServiceInterface { mockService := mock.NewMockServiceInterface(controller) mockService.EXPECT().SomeAction(gomock.Any()).Return(res, err).AnyTimes() return mockService }
Теперь наш сервис ведёт себя по другому. Входные данные теперь просто gomock.Any(), так как нам не важно что придёт на вход, а выходными мы управляем через .Return(). Вот и весь "секрет".
Сгенерированный код лучше хранить в репозитории. Если что-то поменялось в интерфейсе, можно просто перегенерировать код.