Руководство по использованию GoMock Framework

Go

Преамбула

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

  • GoConvey
  • GoStub
  • GoMock
  • Monkey

Читатели могут элегантно использовать фреймворки GoConvey и GoStub, изучив предыдущие три статьи. В этой статье будет представлено использование третьего фреймворка GoMock. Цель состоит в том, чтобы дать читателям возможность освоить правильную позу фреймворка GoConvey + GoStub + GoMock. Тем самым повышая качество тестового кода.

GoMock — это тестовая среда, официально разработанная и поддерживаемая Golang, которая реализует относительно полную функцию имитации на основе интерфейса, которая может быть хорошо интегрирована со встроенным пакетом тестирования Golang, а также может использоваться в других средах тестирования. Тестовая среда GoMock включает пакет GoMock и инструмент mockgen.Пакет GoMock завершает управление жизненным циклом объекта-заглушки, а инструмент mockgen используется для создания исходного файла класса Mock, соответствующего интерфейсу.

Установить

Запустите команду в командной строке:

go get github.com/golang/mock/gomock

После запуска вы обнаружите, что есть подкаталог Github.com/golang/mock в каталоге $ Gopath / SRC, и в этом подкаталоге имеются пакеты Gomock и инструменты Mockgen.

Идем дальше и запускаем команду:

cd $GOPATH/src/github.com/golang/mock/mockgen
go build

Затем в текущем каталоге создается исполняемая программа mockgen.

Переместите программу mockgen в каталог $GOPATH/bin:

mv mockgen $GOPATH/bin

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

-bash: mockgen: command not found

Как правило, это вызвано отсутствием настройки $GOPATH/bin в переменной окружения PATH.

Документация

После установки Gomock Framework вы можете использовать команду GO DOC, чтобы получить документ:

go doc github.com/golang/mock/gomock

Также в сети есть справочная документация, а именноpackage gomock.

инструкции

определить интерфейс

Сначала мы определяем репозиторий интерфейса, который мы собираемся имитировать:

package db

type Repository interface {
    Create(key string, value []byte) error
    Retrieve(key string) ([]byte, error)
    Update(key string, value []byte) error
    Delete(key string) error
}

Репозиторий является элементом тактического дизайна в конструкции полевых драйверов для хранения объектов домена, как правило, настойчиво сохраняются в базе данных, такие как AerosPike, Redis, или EtCD и т. Д. Для доменного слоя только объект поддерживается в репозитории, что не является объектом ухода за последней, что является обязанностью слоя инфраструктуры. Micro Services создает интеграцию интерфейса репозитория на основе параметров развертывания, таких как аэроспезиторий, редисрепозиологии или EtCDrepository.

Предполагая, что существует сохраняемый объект домена Movie, его необходимо сначала сериализовать через json.Marshal, а затем вызвать метод Create репозитория для его сохранения. Если вы хотите найти объект домена по ключу (идентификатору объекта), сначала получите байтовый срез объекта домена с помощью метода Retrieve репозитория, а затем десериализуйте его в объект домена с помощью json.Unmarshal. При изменении данных доменного объекта их необходимо сначала сериализовать через json.Marshal, а затем обновить, вызвав метод Update репозитория. Когда жизненный цикл доменного объекта заканчивается и вот-вот умрет, метод Delete репозитория вызывается непосредственно для его удаления.

Создать файл фиктивного класса

На этот раз на сцену выходит инструмент mockgen. mockgen имеет два режима работы: исходные файлы и отражение.

Режим исходного файла создает файлы фиктивных классов из файла, содержащего определения интерфейса. Он активируется флагом -source. В этом режиме также полезны флаги -imports и -aux_files. Пример:

mockgen -source=foo.go [other options]

В режиме отражения создается файл фиктивного класса путем создания программы, которая понимает интерфейс с отражением, который принимает два параметра, не являющихся флагами: путь импорта и список символов, разделенных запятыми (несколько интерфейсов). Пример:

mockgen database/sql/driver Conn,Driver

Примечание. Первый параметр — это относительный путь на основе GOPATH, второй параметр может указывать несколько интерфейсов, а интерфейсы могут быть разделены только запятыми без пробелов.

Существует исходный файл, содержащий намерение издевательства интерфейса, исходный файл может быть сгенерирован с помощью команды Mock Class Mockgen. Параметры поддержки Mockgen следующие:

  • -source: файл, содержащий список интерфейсов для имитации
  • -destination: файл, в котором хранится фиктивный код. Если вы не установите эту опцию, код будет напечатан на стандартный вывод
  • -package: используется для указания имени пакета исходного файла фиктивного класса. Если вы не установите этот параметр, имя пакета объединяется из mock_ и имени пакета входного файла.
  • -aux_files: см. список дополнительных файлов для разрешения подобных вложенных интерфейсов, определенных в разных файлах. Указанный список элементов разделен запятыми, и элементы имеют вид foo=bar/baz.go, где bar/baz.go — исходный файл, а foo — имя пакета, используемого исходным файлом, указанным -исходный вариант

В простых сценариях вы просто используете параметр -source. В сложных случаях, например, когда файл определяет несколько интерфейсов, и вы хотите имитировать только некоторые из интерфейсов, или интерфейсы вложены, вам необходимо использовать режим отражения. Поскольку ввод параметра -destination слишком длинный, я обычно не использую этот идентификатор, а использую символ перенаправления >, а путь к выходному файлу фиктивного кода должен быть абсолютным путем.

Теперь мы запускаем команду mockgen для создания исходного файла класса Mock репозитория в режиме отражения:

mockgen infra/db Repository > $GOPATH/src/test/mock/db/mock_repository.go

Уведомление:

  1. Выходной каталог test/mock/db должен быть создан заранее, иначе mockgen не запустится.
  2. Если сторонние библиотеки в вашем проекте размещены в каталоге поставщика, вам необходимо скопировать код gomock в $GOPATH/src, Код gomock — github.com/golang/mock/gomock, Это связано с тем, что команда mockgen To доступ к gomock по этому пути во время выполнения

Вы можете видеть, что файл mock_repository.go был сгенерирован в каталоге test/mock/db Фрагмент кода файла выглядит следующим образом:

// Automatically generated by MockGen. DO NOT EDIT!
// Source: infra/db (interfaces: Repository)

package mock_db

import (
    gomock "github.com/golang/mock/gomock"
)

// MockRepository is a mock of Repository interface
type MockRepository struct {
    ctrl     *gomock.Controller
    recorder *MockRepositoryMockRecorder
}

// MockRepositoryMockRecorder is the mock recorder for MockRepository
type MockRepositoryMockRecorder struct {
    mock *MockRepository
}

// NewMockRepository creates a new mock instance
func NewMockRepository(ctrl *gomock.Controller) *MockRepository {
    mock := &MockRepository{ctrl: ctrl}
    mock.recorder = &MockRepositoryMockRecorder{mock}
    return mock
}

// EXPECT returns an object that allows the caller to indicate expected use
func (_m *MockRepository) EXPECT() *MockRepositoryMockRecorder {
    return _m.recorder
}

// Create mocks base method
func (_m *MockRepository) Create(_param0 string, _param1 []byte) error {
    ret := _m.ctrl.Call(_m, "Create", _param0, _param1)
    ret0, _ := ret[0].(error)
    return ret0
}

// Create indicates an expected call of Create
func (_mr *MockRepositoryMockRecorder) Create(arg0, arg1 interface{}) *gomock.Call {
    return _mr.mock.ctrl.RecordCall(_mr.mock, "Create", arg0, arg1)
}
...

Использование фиктивных объектов для стаб-тестирования

После создания исходного файла фиктивного класса вы можете написать тестовые примеры.

Импорт пакетов, связанных с макетами

Пакеты, связанные с макетами, включают тестирование, gmock и mock_db, путь к пакету импорта:

import (
    "testing"
    . "github.com/golang/mock/gomock"
    "test/mock/db"
    ...
)

имитация контроллера

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

Код контроллера выглядит так:

ctrl := NewController(t)
defer ctrl.Finish()

Когда создается фиктивный объект, необходимо внедрить контроллер.Если имеется несколько фиктивных объектов, вводится один и тот же контроллер, как показано ниже:

ctrl := NewController(t)
defer ctrl.Finish()
mockRepo := mock_db.NewMockRepository(ctrl)
mockHttp := mock_api.NewHttpMethod(ctrl)

Внедрение поведения фиктивных объектов

Для внедрения поведения фиктивного объекта контроллер поддерживается картой, а метод соответствует элементу карты. Поскольку в одном варианте использования метод может вызываться несколько раз, тип значения карты — это срез массива. Когда фиктивный объект выполняет внедрение поведения, контроллер добавит поведение. При вызове этого метода контроллер удаляет поведение.

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

mockRepo.EXPECT().Retrieve(Any()).Return(nil, ErrAny)
mockRepo.EXPECT().Create(Any(), Any()).Return(nil)
mockRepo.EXPECT().Retrieve(Any()).Return(objBytes, nil)

objBytes — это результат сериализации объекта домена, например:

obj := Movie{...}
objBytes, err := json.Marshal(obj)
...

При пакетном создании объектов можно использовать ключевое слово Times:

mockRepo.EXPECT().Create(Any(), Any()).Return(nil).Times(5)

При группировании объектов Retrieve вам необходимо внедрить несколько фиктивных поведений:

mockRepo.EXPECT().Retrieve(Any()).Return(objBytes1, nil)
mockRepo.EXPECT().Retrieve(Any()).Return(objBytes2, nil)
mockRepo.EXPECT().Retrieve(Any()).Return(objBytes3, nil)
mockRepo.EXPECT().Retrieve(Any()).Return(objBytes4, nil)
mockRepo.EXPECT().Retrieve(Any()).Return(objBytes5, nil)

Порядок сохранения вызовов действий

По умолчанию порядок вызова поведения может не соответствовать порядку внедрения поведения фиктивного объекта, то есть порядок не сохраняется. Если вы хотите сохранить порядок, есть два пути:

  1. Сохранение порядка через ключевое слово After
  2. Сохранение заказа через ключевое слово InOrder

Пример кода сохранения ордера реализованного ключевым словом After:

retrieveCall := mockRepo.EXPECT().Retrieve(Any()).Return(nil, ErrAny)
createCall := mockRepo.EXPECT().Create(Any(), Any()).Return(nil).After(retrieveCall)
mockRepo.EXPECT().Retrieve(Any()).Return(objBytes, nil).After(createCall)

Пример кода сохранения заказа, реализованного ключевым словом InOrder:

InOrder(
    mockRepo.EXPECT().Retrieve(Any()).Return(nil, ErrAny)
    mockRepo.EXPECT().Create(Any(), Any()).Return(nil)
    mockRepo.EXPECT().Retrieve(Any()).Return(objBytes, nil)
)

Видно, что сохранение порядка, реализованное ключевым словом InOrder, проще и естественнее, поэтому этот способ рекомендуется. На самом деле ключевое слово InOrder является синтаксическим сахаром для After, а исходный код выглядит следующим образом:

// InOrder declares that the given calls should occur in order.
func InOrder(calls ...*Call) {
    for i := 1; i < len(calls); i++ {
        calls[i].After(calls[i-1])
    }
}

После того, как внедрение поведения фиктивного объекта будет в порядке, если порядок вызовов поведения несовместим с ним, будет инициирован сбой теста. То есть для приведенного выше примера, если во время выполнения тестового примера порядок вызова методов репозитория не соответствует порядку «Извлечение» -> «Создать» -> «Извлечение», тест завершится ошибкой.

Внедрение фиктивных объектов

После того, как поведение фиктивного объекта введено в контроллер, нам нужно внедрить фиктивный объект в интерфейс, чтобы фиктивный объект вступил в силу в тесте. Прежде чем использовать платформу GoStub, многие люди использовали земные методы, такие как Set. У этого метода есть недостаток: при выполнении тест-кейса интерфейс не откатывается к реальному объекту, что может повлиять на выполнение других тест-кейсов. Поэтому автор настоятельно рекомендует использовать фреймворк GoStub для завершения внедрения фиктивных объектов.

stubs := StubFunc(&redisrepo.GetInstance, mockDb)
defer stubs.Reset()

тестовая демонстрация

Есть несколько основных принципов написания тестовых случаев, давайте рассмотрим их вместе:

  1. Каждый тест-кейс фокусируется только на одной проблеме, не пишите большие и всеобъемлющие тест-кейсы.
  2. Тест-кейсы — это черный ящик
  3. Тестовые случаи не зависят друг от друга, и каждый вариант использования должен гарантировать завершение своей собственной предварительной и последующей обработки.
  4. Тестовые случаи не должны нарушать код продукта.
  5. ...

В соответствии с основным принципом мы не должны делить фиктивный контроллер между несколькими тестовыми примерами тестовой функции, поэтому у нас есть следующее демо:

func TestObjDemo(t *testing.T) {
    Convey("test obj demo", t, func() {
        Convey("create obj", func() {
            ctrl := NewController(t)
            defer ctrl.Finish()
            mockRepo := mock_db.NewMockRepository(ctrl)
            mockRepo.EXPECT().Retrieve(Any()).Return(nil, ErrAny)
            mockRepo.EXPECT().Create(Any(), Any()).Return(nil)
            mockRepo.EXPECT().Retrieve(Any()).Return(objBytes, nil)
            stubs := StubFunc(&redisrepo.GetInstance, mockRepo)
            defer stubs.Reset()
            ...
        })

        Convey("bulk create objs", func() {
            ctrl := NewController(t)
            defer ctrl.Finish()
            mockRepo := mock_db.NewMockRepository(ctrl)
            mockRepo.EXPECT().Create(Any(), Any()).Return(nil).Times(5)
            stubs := StubFunc(&redisrepo.GetInstance, mockRepo)
            defer stubs.Reset()
            ...
        })

        Convey("bulk retrieve objs", func() {
            ctrl := NewController(t)
            defer ctrl.Finish()
            mockRepo := mock_db.NewMockRepository(ctrl)
            objBytes1 := ...
            objBytes2 := ...
            objBytes3 := ...
            objBytes4 := ...
            objBytes5 := ...
            mockRepo.EXPECT().Retrieve(Any()).Return(objBytes1, nil)
            mockRepo.EXPECT().Retrieve(Any()).Return(objBytes2, nil)
            mockRepo.EXPECT().Retrieve(Any()).Return(objBytes3, nil)
            mockRepo.EXPECT().Retrieve(Any()).Return(objBytes4, nil)
            mockRepo.EXPECT().Retrieve(Any()).Return(objBytes5, nil)
            stubs := StubFunc(&redisrepo.GetInstance, mockRepo)
            defer stubs.Reset()
            ...
        })
        ...
    })
}

резюме

В этой статье подробно рассматривается использование платформы GoMock. В ней не только приводится стандартное использование с примерами, но также перечисляются многие ключевые моменты. Наконец, простая тестовая демонстрация иллюстрирует правильную позу для комбинации GoConvey + GoStub + GoMock. Есть надежда, что читатели сделают выводы из одного примера и в то же время интегрируют основное содержание предыдущих трех статей, напишут высококачественный тестовый код и, в конечном счете, улучшат качество продукта.

На данный момент мы уже знаем:

  1. Глобальные переменные можно складывать через фреймворк GoStub
  2. Процесс можно завалить через фреймворк GoStub
  3. Функции можно нагромождать через фреймворк GoStub
  4. интерфейс может быть нагроможден кадром GoMock

Итак, возникает вопрос, метод заключается в том, чтобы загнать сваи через бога-коня? Ответ мы дадим в следующей статье.