При работе с моками создаётся объект, который доступен извне так же, как настоящий, но разработчик полностью контролирует его поведение. Именно этот объект и называется mock.

Note

В языке Go мок-тестирование особенно удобно благодаря концепции интерфейсов. По сути, всё, что нужно для создания объекта-заглушки, — это удовлетворить интерфейсу реального объекта. В других ООП-языках — вроде Python или Java — мок-тестирование также существует, однако может быть сложнее в случае сложной иерархии наследования.

Это бывает полезно, когда:

  • нужно протестировать только работу бизнес-логики;
  • процессы занимают много времени и его можно сэкономить при тестировании;
  • нельзя или нежелательно при тестировании выполнять какую-то операцию, например отправку email или уведомлений;
  • невозможно развернуть копию БД или она представляет собой чёрный ящик;
  • сложно тестировать необходимые состояния во внешних источниках данных и проще установить нужные граничные условия на моках.

Проще всего понять принцип работы с моками на примере.

Предположим, есть БД, которую нельзя использовать для тестирования, но надо проверить, правильно ли работает написанный код для работы с ней. Возьмём простейший случай: пакет для работы с БД имеет тип DB и метод для проверки существования пользователя по его email. Метод UserExists возвращает true, если пользователь с указанным адресом существует, и false, если нет.

func (db *DB) UserExists(email string) bool 

В прошлых темах рассматривалось понятие интерфейсного типа, в котором описывается только поведение (методы) какого-то объекта. При этом структура и его внутренняя реализация не имеют значения — можно описать набор методов для работы с БД в виде интерфейса.

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

type DBStorage interface {
    UserExists(email string) bool
}
 
 
// обратите внимание, что DBStorage передаётся в функцию в качестве параметра, таким образом мы можем при тестировании подменить реальную БД тестовой заглушкой.
func NewUser(db DBStorage, email string) error {
    if db.UserExists(email) {
        return fmt.Errorf(`user with '%s' email already exists`, email)
    }
    // добавляем запись
    return nil
} 

Здесь определены интерфейсный тип DBStorage и функция NewUser, в которой происходит проверка на существование пользователя с таким же почтовым ящиком. В продакшене эту функцию будем вызывать для переменной типа DB, а сейчас напишем для неё тест.

Если есть много вариантов для проверки, то для тестирования лучше использовать таблицы (table-driven tests) с входящими данными и ожидаемыми результатами:

import (
    "github.com/stretchr/testify/require"
)
 
// тип объекта-заглушки
type DBMock struct {
    emails map[string]bool
}
 
// для удовлетворения интерфейсу DBStorage реализуем  
func (db *DBMock) UserExists(email string) bool {
    return db.emails[email]
}
// вспомогательный метод, для подсовывания тестовых данных
func (db *DBMock) addUser(email string) {
    db.emails[email] = true
}
 
func TestNewUser(t *testing.T) {
    errPattern := `user with '%s' email already exists`
    tbl := []struct {
        name    string
        email   string
        preset  bool
        wanterr bool
    }{
        {`want success`, `gregorysmith@myexampledomain.com`, false, false},
        {`want error`, `johndoe@myexampledomain.com`, true, true},
    }
    for _, item := range tbl {
        t.Run(item.name, func(t *testing.T) {
            // создаём объект-заглушку 
            dbMock := &DBMock{emails: make(map[string]bool)}
            if item.preset {
                dbMock.addUser(item.email)
            }
             // выполняем наш код, передавая объект-заглушку
            err := NewUser(dbMock, item.email)
            if !item.wanterr {
                require.NoError(err)
            } else {
                require.EqualError(t, err, fmt.Sprintf(errPattern, err.email))
            }
        })
    }
}

В тестовой функции TestNewUser проверяем, какой результат возвращает функция NewUser в зависимости от того, существует пользователь с таким email или нет. Таким образом можно протестировать поведение своей функции без обращения к реальной базе данных. В примере на каждую итерацию создаём мок и при необходимости добавляем туда email, чтобы проверить различные варианты.

Моки можно использовать не только для подмены операций, но и для получения детальной информации, например количества вызовов функции, контроля параметров и т. д. Для этого достаточно добавить нужные поля в структуру заглушки и изменять их при каждом вызове интерфейсного метода:

type DBMock struct {
    emails  map[string]bool
    counter int
}
 
func (db *DBMock) UserExists(email string) bool {
    db.counter++
    return db.emails[email]
} 

Итак, мы разобрались с общими принципами работы моков, которые дают дополнительные возможности для тестирования ПО. В простых случаях можно самостоятельно создавать подобные тесты, а на практике лучше использовать готовые библиотеки для тестирования с использованием моков:

Дополнительные материалы

GODEVPL10


📂 Go | Последнее изменение: 01.09.2024 14:21