欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

如何用golang编写单元测试用例

程序员文章站 2022-04-26 09:40:39
...

最近帮忙给一个项目补充单元测试,有一些单测比较不好写, 到网上查了一下,发现有很多有意思的写法,特此总结一下

net.dial 方法的单测

如果我们代码里面使用了net.Dial()去访问外部的tcp or udp 端口,然后使用返回的Conn对象去处理里面的数据,我们该如何对这个Conn对象进行mock呢?

  1. 最好想的办法是自己实现一个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)
}
  1. 其实最简单的方法是这种
server, client := net.Pipe()
go func(){
    server.Close()
}()

client.Close()

mock 本地service 和 第三方 package

我们直接用代码说话吧

  1. 我们使用如下的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
}
  1. 我们写个方法调用上面的服务

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
}
  1. 然后再为上面的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进行单元测试,除非可以调用这个数据库方法。

下面我们就来解决这个问题
我们需要重构我们的代码。

  1. 我们定义一个interface
type registrationPreChecker interface {
    userExists(string) bool
}
  1. 然后我们定义一个struct实现这个接口
type regPreCheck struct {}
func (r regPreCheck) userExist(email string) bool {
    return userdb.UserExist(email)
}
  1. 我们定义一个package-level 的registrationPreCheckerinterface 类型的变量, 并在init方法里面调用它
var regPreCond registrationPreChecker

func init() {
    regPreCond = regPreCheck{}
}
  1. 最后我们重写一下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方法,而不是简单的trueorfalse这个是为了要在运行期分配mock, 而不是在编译器分配
你可以在 Test 方法里面找到具体的mock function

但是我们还没结束,上面的方式还是有问题的。

我们在上面使用了全局变量, 虽然我们没有在实际过程中更新 这个全局变量, 但是这会破坏并发测试

想要避免上述问题其实也很简单, 直接把registrationPreChecker的实例通过参数传入RegisterUser()方法里面就可以了

gomock package

其实gomock package 也是以自定义interface为入口,将自定义返回数据注入到mockobject 里面去。实现原理基本与上面一样, 使用gomock当然会简单一些,不过个人还是偏向于上面那种纯手动的。。