Опубликовано: 23 июля 2022 г. 23:10 | Автор: echodiv | Категория: Разработка | 👁 3255

Тестирование в GO с использованием "моков"

Важной частью создания програмного обеспечения является покрытие кодовой базы тестами.

 

В основании всем известной пирамиды тестрования лежат юнит тесты. Не будем с ней спорить, а будем писать юниты, много юнит тестов. Не всегда можно просто взять и написать тест, иногда для тестирования нам нужно заглушить какие-то зависимости, будь то доступ в базу данных, сетевое взаимодействие или какая-нибудь магия.

 

Естественно, для этого придумали "моки". Придумали не только их, но в данной статье речь пойдет только про моки и немного про кодогенерацию, не писать же их руками. 

У нас есть некоторый оъект, у которого есть внешняя зависимость. Внешнюю зависимость мы подсовывем при инстанцировании, не вызываем её непонятно откуда, как минимум, это не красиво. Также мы используем интерфейс для внешней зависимости.

 

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(). Вот и весь "секрет".

Сгенерированный код лучше хранить в репозитории. Если что-то поменялось в интерфейсе, можно просто перегенерировать код.