Если вы видите что-то необычное, просто сообщите мне. Skip to main content

Как мокать? Go способ.

У Go есть встроенный фреймворк тестирования предоставленный testing пакетом, это позволяет писать тесты проще, но тогда как мы пишем более сложные тесты которые требуют моков? В этой статье, мы изучим как взять преимущества структур и интерфейсов в Go, чтобы смокать любой сервис или библиотеку которую используем, без использования сторонних инструментов и библиотек. Начнем с определения нашей системы для понимания того. что мы будем делать с тестом и моком.

Система.

Наша система имеет 2 компонента:

  • "Наш сервис" который у нас есть и мы создаем
  • "Сторонние сервисы и библиотеки" которые взаимодействуют с базой данных и мы используем для работоспособности нашего сервиса. Теперь так как мы строим "Наш сервис", мы хотим написать независимую единицу для "Нашего сервиса" но так как мы используем функционал стороннего сервиса или библиотеки в нашем сервисе, если мы тестируем без моков, мы будет производить интеграционное тестирование, что иногда сложно и более временно затратно. Для демонстрации, мы напишем простую библиотеку которая проверяет существует ли пользователь внутри map. Наш сервис будет нужен для хранения бизнес логики, и это станет сторонней библиотекой внутри нашей стистемы.

Код сторонней библиотеки

package userdb

// db act as a dummy package level database.
var db map[string]bool

// init initialize a dummy db with some data
func init() {
	db = make(map[string]bool)
	db["ankuranand@dummy.org"] = true
	db["anand@example.com"] = true
}

// UserExists check if the User is registered with the provided email.
func UserExists(email string) bool {
	if _, ok := db[email]; !ok {
		return false
	}
	return true
}

Сервис

Регистрация пользователя использует сторонний код, для проверки наличия пользователя. Если пользователь не существует, то сервис просто возвращает ошибку,и в обратном случае выполняет обычную логику.

package simpleservice

import (
	"fmt"
	"log"

	"github.com/ankur-anand/mocking-demo/userdb"
)

// User encapsulate a user in the system.
type User struct {
	Name     string `json:"name"`
	Email    string `json:"email"`
	UserName string `json:"user_name"`
}

// RegisterUser will register a User if only User has not been previously
// registered.
func RegisterUser(user User) error {
	// check if user is already registered
	found := userdb.UserExists(user.Email)
	if found {
		return fmt.Errorf("email '%s' already registered", user.Email)
	}
	// carry business logic and Register the user in the system
	log.Println(user)
	return nil
}
package simpleservice

import "testing"

func TestCheckUserExist(t *testing.T) {
	user := User{
		Name:     "Ankur Anand",
		Email:    "anand@example.com",
		UserName: "anand",
	}

	err := RegisterUser(user)
	if err == nil {
		t.Error("Expected Register User to throw and error got nil")
	}
}

Если мы посмотрим на функцию theRegisterUser. Она вызывает Функцию userdb.UserExistкоторая является для нас стороннеё библиотекой и мы не можем протестировать нашу RegisterUser функцию без её вызова.

Моки

Попробуем это исправить моками.

Мок-объекты соответствуют требованиям к интерфейсу и заменяют более сложные настоящие объекты.

Мок-объекты соответствуют требованиям к интерфейсу.

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

// registrationPreChecker validates if user is allowed to register.
type registrationPreChecker interface {
    userExists(string) bool
}

InterfaceРеализация Implementation.интерфейса.

userExists функция нашего нового интерфейса просто оборачивает вызова настоящего вызова к стороннему сервису.

type regPreCheck struct {}
func (r regPreCheck) userExists(email string) bool {
    return userdb.UserExist(email)
}

Теперь создадим, переменную на уровне пакета типа registrationPreChecker и присвоим экземпляр regPreCheck внутри init функции.

var regPreCond registrationPreChecker

func init() {
   regPreCond = regPreCheck{}
}

Так как regPreCond является типом registrationPreChecker который проверяет существования пользователя, whichмы validatesможем the user exists or not, we can use this inside ourиспользовать RegisterUser function.фнукцию. SoПоэтому insteadвместо ofпрямого directlyвызова callingфункции userdb.UserExist functionвнутри insideфункции RegisterUser functionмы weвызовем willчерез callреализацию it through our interface implementation.интерфейса.

// check if user is already registered
found := regPreCond.userExist(user.Email)

RefactoredИзменный code:код:

If

package weunitservice

runimport (
	"fmt"
	"log"

	"github.com/ankur-anand/mocking-demo/userdb"
)

// User encapsulate a user in the gosystem.
testtype againUser itstruct passes{
	becauseName     westring haven’t`json:"name"`
	changedEmail    anystring behavior`json:"email"`
	ofUserName ourstring function.`json:"user_name"`
But}

let's// see how this makes unit testing of our service so easy—through mocks.

Writing mocks

Let’s see the complete code of our test first.

Mock objects meet the interface requirements.requirements Hereof, our’s// and stand in for, more complex real ones type registrationPreChecker interface { userExists(string) bool } type regPreCheck struct{} func (r regPreCheck) userExists(email string) bool { return userdb.UserExist(email) } var regPreCond registrationPreChecker func init() { regPreCond = regPreCheck{} } // RegisterUser will register a User if only User has not been previously // registered. func RegisterUser(user User) error { // check if user is already registered found := regPreCond.userExist(user.Email) if found { return fmt.Errorf("email '%s' already registered", user.Email) } // carry business logic and Register the user in the system log.Println(user) return nil }

Если мы запустим test опять он пройет так как мы не трогали поведение нашей функции. Теперь давайте посмотрим как заставить наше тестирование проходить с помощью моков.

Написание моков

Для начала посмотрим на полный код:

package unitservice

import "testing"

// This helps in assigning mock object implementsat the runtime instead of compile time
var userExistsMock func(email string) bool

type preCheckMock struct{}

func (u preCheckMock) userExists(email string) bool {
	return userExistsMock(email)
}

func TestRegisterUser(t *testing.T) {
	user := User{
		Name:     "Ankur Anand",
		Email:    "anand@example.com",
		UserName: "anand",
	}

	regPreCond = preCheckMock{}
	userExistsMock = func(email string) bool {
		return false
	}

	err := RegisterUser(user)
	if err != nil {
		t.Fatal(err)
	}

	userExistsMock = func(email string) bool {
		return true
	}
	err = RegisterUser(user)
	if err == nil {
		t.Error("Expected Register User to throw and error got nil")
	}
}

Мок объекты отвечают требованиям интерфейса. Тут наш мок объект реализует registrationPreChecker interface.интерфейс.

type preCheckMock struct{}

func (u preCheckMock) userExists(email string) bool {
   return userExistsMock(email)
}

MockРеализация implementationмока is returning anвозвращает userExistsMockuserExistMock functionтип typeфункции hereвместо insteadпрямого ofвозвращения directlytrue returningили truefalse. orЭто false.помогает Thisв helpsназначении inмока assigningво mockвремя atработы runtimeвместо insteadво ofвремя compile-time.компиляции. YouВы canможете seeувидеть thisэто in theв TestRegisterUser function.фукнции.

и заменяют более сложные настоящие объекты

regPreCond = preCheckMock{}

We simply assigned our regPreCond of type registrationPreChecker which validates the user exists or not with our mock implementation during runtime of our test. As you can see in TestRegisterUser function.

func TestRegisterUser(t *testing.T) {
   user := User{
      Name:     "Ankur Anand",
      Email:    "anand@example.com",
      UserName: "anand",
   }

   regPreCond = preCheckMock{}
   userExistsMock = func(email string) bool {
      return false
   }

   err := RegisterUser(user)
   if err != nil {
      t.Fatal(err)
   }

   userExistsMock = func(email string) bool {
      return true
   }
   err = RegisterUser(user)
   if err == nil {
      t.Error("Expected Register User to throw and error got nil")
   }
}

But we are done yet!

We said Go way, right but right now there is an issue, with the way we have refactored it.

We have used the Global variable and event though we are not updating the variable during the actual run, this is going to break the parallel test.

There are different ways we can fix this, In our case, we are going to pass this registrationPreChecker as a dependency to our function and will introduce a New Constructor function that will create a default registrationPreChecker type that can be used during actual usage, and since we are also passing this as a dependency, we can pass our mock implementation as a parameter during this.

func NewRegistrationPreChecker() RegistrationPreChecker {
   return regPreCheck{}
}

// RegisterUser will register a User if only User has not been previously
// registered.
func RegisterUser(user User, regPreCond RegistrationPreChecker) error {
   // check if user is already registered
   found := regPreCond.userExists(user.Email)
   if found {
      return fmt.Errorf("email '%s' already registered", user.Email)
   }
   // carry business logic and Register the user in the system
   log.Println(user)
   return nil
}

Let’s modify our test code too. So instead of modifying the package level variable, we are now explicitly passing it as a dependency inside our RegisterUser function.

func TestRegisterUser(t *testing.T) {
   user := User{
      Name:     "Ankur Anand",
      Email:    "anand@example.com",
      UserName: "anand",
   }

   regPreCond := preCheckMock{}
   userExistsMock = func(email string) bool {
      return false
   }

   err := RegisterUser(user, regPreCond)
   if err != nil {
      t.Fatal(err)
   }

   userExistsMock = func(email string) bool {
      return true
   }
   err = RegisterUser(user, regPreCond)
   if err == nil {
      t.Error("Expected Register User to throw and error got nil")
   }
}

ankur-anand/go-test-demo You can't perform that action at this time. You signed in with another tab or window. You signed out in another tab or… github.com

Conclusion

Well, while this is not the only way to write a mock in Go, but with this hopefully you now have the general idea of mocking and how to take advantage of structs and interfaces in Go to mock any artifacts you might be using, without any external mocking library.