Как мокать? 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
}
Реализация интерфейса.
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
который проверяет существования пользователя, мы можем использовать RegisterUser
фнукцию. Поэтому вместо прямого вызова функции userdb.UserExist
внутри функции RegisterUser
мы вызовем через реализацию интерфейса.
// check if user is already registered
found := regPreCond.userExist(user.Email)
Изменный код:
package unitservice
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"`
}
// Mock objects meet the interface requirements of,
// 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 at 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
интерфейс.
type preCheckMock struct{}
func (u preCheckMock) userExists(email string) bool {
return userExistsMock(email)
}
Реализация мока возвращает userExistMock
тип функции вместо прямого возвращения true
или false
. Это помогает в назначении мока во время работы вместо во время компиляции. Вы можете увидеть это в TestRegisterUser
фукнции.
иИх заменяют более сложные настоящие объекты
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"Способ nowGO", 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передать registrationPreCheckerregistrationPreChekerasкак aзависимость dependencyнашей toфункции ourи functionпредставим andновую willфункцию introduceконструктора aкоторая Newбудет Constructorсоздавать functionпо that will create a defaultумолчанию registrationPreCheckerregitrationPreChecktypeтип, 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в aGO, mockно inс Go,этим butя withнадуюсь thisу hopefullyвас youпоявиться nowобщее haveпонимание theмоков generalи ideaкак ofиспользовать mockingструктуры andи howинтерфейсы toв takeGO advantageчтобы ofмокать structsлюбые andартефакты interfacesкоторые inвам Goпонадобятся, toбез mockвнешних any artifacts you might be using, without any external mocking library.блиблиотек.