Как мокать? Go способ.
Go has a built-in testing framework provided by the testing package, that makes writing tests easier, but how do we write a more complicated test that requires mocking?
In this post, we will learn how to take advantage of structs and interfaces inУ Go toесть mockвстроенный anyфреймворк serviceтестирования orпредоставленный librarytesting
youпакетом, mightэто beпозволяет using,писать withoutтесты usingпроще, anyно 3rdтогда partyкак toolsмы andпишем libraries.более Weсложные willтесты startкоторые byтребуют definingмоков?
ourВ systemэтой toстатье, understandмы whatизучим weкак areвзять goingпреимущества toструктур testи andинтерфейсов mock.в Go, чтобы смокать любой сервис или библиотеку которую используем, без использования сторонних инструментов и библиотек.
Начнем с определения нашей системы для понимания того. что мы будем делать с тестом и моком.
SystemСистема.
OurНаша systemсистема hasимеет two2 components.компонента:
“Our"НашService”сервис"thatкоторыйweуownнасandестьbuild.и мы создаем“Third-party"Сторонниеserviceсервисыorиlibrary”библиотеки"thatкоторыеinteractsвзаимодействуютwithсsomeбазойdatabaseданныхandиweмыuseиспользуемtheдляfunctionalityработоспособностиofнашегоitсервиса.inТеперьourтакservice.какNowмыSinceстроимwe"Нашareсервис",buildingмы“OurхотимService”написатьweнезависимуюwantединицуtoдляwrite"Нашегоanсервиса"independentноunitтакtestкакforмы“OurиспользуемService”функционалbutстороннегоasсервисаweилиuseбиблиотекиtheвfunctionalityнашемofсервисе,third-partyеслиserviceмыorтестируемlibraryбезinмоков,ourмыserviceбудетifпроизводитьweинтеграционноеtestтестирование,withoutчтоmock’sиногдаweсложноwillиbeболееdoingвременноtheзатратно.integrationДляtestingдемонстрации,whichмыisнапишемsometimesпростуюcomplexбиблиотекуandкотораяmoreпроверяетtime-consuming.существуетForлиourпользовательdemonstration purpose, we’ll write a simple library that checks if a user exists or not inside aвнутри map.OurНашServiceсервисwillбудетuseнуженthisдляtoхраненияcarryбизнесoutлогики,itsиbusinessэтоlogicстанетandстороннейthisбиблиотекойwillвнутриbecomeнашейthe third-party library inside our system.стистемы.
Third-partyКод Libraryсторонней Codeбиблиотеки
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
}
ServiceСервис
RegistersРегистрация aпользователя user.использует Itсторонний usesкод, aдля thirdпроверки partyналичия toпользователя. checkЕсли forпользователь theне user’sсуществует, existence.то Ifсервис theпросто userвозвращает exists,ошибку,и theв serviceобратном simplyслучае returnsвыполняет anобычную error otherwise it carries out the usual business logic.
Code:логику.
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")
}
}
IfЕсли youмы takeпосмотрим aна lookфункцию attheRegisterUser
. theRegisterUserОна function:вызывает Функцию userdb.UserExist
которая является для нас стороннеё библиотекой и мы не можем протестировать нашу RegisterUser
функцию без её вызова.
Моки
Попробуем это исправить моками.
Про мокинг:
Разберемся. Мок объект встречает требования интерфейса. Для этого нам нужно переделать код нашего сервиса. Первое, мы должны определить требования интерфейса, для того чтобы реализовать наш мок. В нашем случае, нам нужен интерфейс который внутренний для пакета который предоставляет тестирование существует пользователь или нет.
// RegisterUser if the user is not registered before
func RegisterUser(user User) error {
if userdb.UserExist(user.Email) {
return fmt.Errorf("email '%s' already registered",
user.Email)
}
// ...code for registering the user...
log.Println(user)
return nil
}
It calls the function userdb.UserExist which is provided by our third-party library and currently we cannot unit test our RegisterUser function without doing a third party call.
Mocks
Let’s try to fix this by mocking.
About mocking:
Mock objects meet the interface requirements of, and stand in for, more complex real ones. Thank you, Wikipedia!
Let’s break it up.
Mock objects meet the interface requirements.
To do so we have to refactor our service code. First, we have to define our interface requirement that our mock going to implement. In our case, we need an interface that is just internal to the package and provides a way to test if a user exists or not.
// registrationPreChecker validates if user is allowed to register.
type registrationPreChecker interface {
userExists(string) bool
}
# Interface Implementation.
userExists function of our new defined interface simply wraps the call to the actual call to third party service. type regPreCheck struct {} func (r regPreCheck) userExists(email string) bool { return userdb.UserExist(email) } Next, we will create a package-level variable of type registrationPreChecker and assign an instance of regPreCheck to it within init function. var regPreCond registrationPreChecker
func init() { regPreCond = regPreCheck{} } As regPreCond is of type 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 we run the go test again it passes because we haven’t changed any behavior of our function. 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. Here our’s mock object implements the registrationPreChecker interface.
type preCheckMock struct{}
func (u preCheckMock) userExists(email string) bool {
return userExistsMock(email)
}
Mock implementation is returning an userExistsMock function type here instead of directly returning true or false. This helps in assigning mock at runtime instead of compile-time. You can see this in the TestRegisterUser function. 2. and stand in for, more complex real ones
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")
}
}
Source Code: 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.