如何用golang编写单元测试用例
程序员文章站
2022-04-26 09:40:39
...
最近帮忙给一个项目补充单元测试,有一些单测比较不好写, 到网上查了一下,发现有很多有意思的写法,特此总结一下
net.dial 方法的单测
如果我们代码里面使用了net.Dial()
去访问外部的tcp or udp 端口,然后使用返回的Conn
对象去处理里面的数据,我们该如何对这个Conn
对象进行mock呢?
- 最好想的办法是自己实现一个server端,使用
net.Listen()
本地的一个端口,并且撰写自己的handler方法,在handler方法里面返回想要的值。最后在单测里面直接调用这个端口就可以在返回的conn
对象中获取到mock的值了。
下面写一个简单的例子
go func() {
conn, err := net.Dial("tcp", 3000)
if err != nil {
xxx
}
defer conn.close()
xxx
}
l, err := net.Listen("tcp", 3000)
defer l.Close()
for {
conn, err := l.Accept()
defer conn.Close()
handle(conn)
}
- 其实最简单的方法是这种
server, client := net.Pipe()
go func(){
server.Close()
}()
client.Close()
mock 本地service 和 第三方 package
我们直接用代码说话吧
- 我们使用如下的service来当做db(第三方服务)
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["[email protected]"] = true
db["[email protected]"] = 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
}
- 然后再为上面的
RegisterUser
写一个单元测试
package simpleservice
import "testing"
func TestCheckUserExist(t *testing.T){
user := User{
Name: "Ankur Anand"
Email: "[email protected]"
UserName: "anand"
}
err := RegisterUser(user)
if err == nil {
t.Error("Expect Register User to throw and error got nil")
}
}
我们可以看到上面 2 中的 RegisterUser
方法内部使用了一个 userdb.UserExist
访问数据库的方法, 按理说
我们无法调用RegisterUser
进行单元测试,除非可以调用这个数据库方法。
下面我们就来解决这个问题
我们需要重构我们的代码。
- 我们定义一个interface
type registrationPreChecker interface {
userExists(string) bool
}
- 然后我们定义一个struct实现这个接口
type regPreCheck struct {}
func (r regPreCheck) userExist(email string) bool {
return userdb.UserExist(email)
}
- 我们定义一个package-level 的
registrationPreChecker
interface 类型的变量, 并在init
方法里面调用它
var regPreCond registrationPreChecker
func init() {
regPreCond = regPreCheck{}
}
- 最后我们重写一下
RegisterUser
代码
func RegisterUser(user User) error {
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
}
事前准备做完了,我们来写下mock脚本
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: "[email protected]"
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")
}
}
Mock 实现返回了 userExistsMock
方法,而不是简单的true
orfalse
这个是为了要在运行期分配mock, 而不是在编译器分配
你可以在 Test 方法里面找到具体的mock function
但是我们还没结束,上面的方式还是有问题的。
我们在上面使用了全局变量, 虽然我们没有在实际过程中更新 这个全局变量, 但是这会破坏并发测试
想要避免上述问题其实也很简单, 直接把registrationPreChecker
的实例通过参数传入RegisterUser()
方法里面就可以了
gomock package
其实gomock package 也是以自定义interface为入口,将自定义返回数据注入到mockobject 里面去。实现原理基本与上面一样, 使用gomock当然会简单一些,不过个人还是偏向于上面那种纯手动的。。
推荐阅读