一、 背景
关于为什么要单元测试,记得有的人说过,从单元测试,到业务测试再到UI测试,越底层发现错误越快,修改的成本也越低。就自己来说,最近用到了golang的项目,发现 golang 的单元你测试不如 java 的 springboot的好用,因此做了个技巧的总结,希望能方便单元测试。
二、小技巧
1、golang 的 mock
有时我们的代码依赖外部组件,但是外部组件无法提供单测环境,或者按正常流程运行不起来,这个时候就可以考虑用mock的方式处理,专注于自己模块的测试。
使用的组件:testify,这个简直是神器,建议使用。
mock 使用方式
a、创建一个 dog_service,实现 Speak 的方法
package service
import "fmt"
type DogService struct {
}
func (dog DogService) Speak(times int) int {
for a := 0; a < times; a++ {
fmt.Printf("汪! %d\n", a)
}
return times
}
b、创建声音sevice,可以传入其他实现了SpeakService的实例。
voice_service
package service
type SpeakService interface {
Speak(times int) int //发声次数
}
type MyService struct {
SpeakService SpeakService
}
func (m MyService)SendVoice() {
m.SpeakService.Speak(3)
}
c、在测试文件中mock Dog 的 speak 方法
main_test
package main
import (
"fmt"
"github.com/stretchr/testify/mock"
"gotips/service"
"testing"
)
//1、mock struct
type DogMock struct {
mock.Mock
}
//2、args 对应 return 的参数列表,Called 对应 On 方法
func (m *DogMock) Speak(times int) int {
fmt.Println("Mocked charge notification function")
fmt.Printf("Value passed in: %d\n", times)
args := m.Called(times)
return args.Int(0)
}
//3、执行调用
func TestSendVoice(t *testing.T) {
dogService := new(DogMock)
dogService.On("Speak", 3).Return(3)
myService := service.MyService{dogService}
myService.SendVoice()
}
使用 mock 后可以更细粒度的测试代码模块。目前还不能像php、java那样就行部分mock,这是作者的回复 https://github.com/stretchr/testify/issues/29,go实现这个还是有困难。
2、测试 golang 私有方法
私有方法测试的话就在同一个包下,如下面的例子。
private_p
package service
import "fmt"
func eat() {
fmt.Sprintf("test")
}
private_p_test
package service
import "testing"
func TestEat(t *testing.T) {
eat()
}
关于是否要测试私有方法,stackoverflow 有些讨论
https://stackoverflow.com/questions/105007/should-i-test-private-methods-or-only-public-ones
多数认为,一般不要测试私有方法,这会破坏封装性。
3、golang fixture
单元测试的资源文件应该放在那里呢?查了下标准库,可以放到包下面的 testdata 文件夹。go build 的时候回忽略该文件夹。go 运行单元测试的时候会把当前package设置为当前目录,因此可以直接使用当前目录加载。例如下面里例子,hello.txt 放在 controller 的 testdata 中
|____controller
| |____testdata
| | |____hello.txt
| |____upload.go
| |____upload_test.go
path := "testdata/hello.txt"//要上传文件所在路径
file, _ := os.Open(path)
defer file.Close()
4、idea 传参问题
一般的大型项目会涉及到环境切换,测试环境、生产环境等。因此如果在 idea 中使用系统自带的工具跑单测需要把环境参数传进去。查询官方的资料会发现 idea 可以设置运行的测试模板,而且使用成本很低,因此采用这种方式传参。设置方式如下图:
读取参数例子
args := os.Args
env := ""
for _, v := range args {
if strings.HasPrefix(v, "env=") {
env = string([]byte(v)[4:])
break
}
}
fmt.Println("env=",env)
5、包循环引用问题
正常开发中如果a引用了b包,b引用了a包,然后在测试b的单元单测会出现循环引用问题。这个官方的代码中已经有了解决方法,是把b的单元测试包改为 b_test,这样就完美的解决了这个问题。当然b_test不能被其他包引用,部分版本出现过包找不到的空指针问题,因为b_test和包的文件名不一致。后面的测试文件上传有个比较完整的例子。
package controller_test
6、golang http test
普通的 post、get请求比较简单,这里介绍下怎样测试文件上传。
func Bootstrap() *gin.Engine {
args := os.Args
env := ""
for _, v := range args {
if strings.HasPrefix(v, "env=") {
env = string([]byte(v)[4:])
break
}
}
fmt.Println("env=",env)
engine := gin.New()
engine.MaxMultipartMemory = 1024 * 1024 * 1024
engine.POST("/upload", controller.Upload)
return engine
}
upload_test.go
package controller_test //防止循环引用
import (
"bytes"
"gotips/bootstrap"
"io"
"mime/multipart"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"testing"
)
func TestUpload(t *testing.T) {
//添加参数
params := map[string]string{}
params["test"] = "100"
body := &bytes.Buffer{}
writer := multipart.NewWriter(body)
for k,v := range params{
writer.WriteField(k,v)
}
//添加文件
path := "testdata/hello.txt"//要上传文件所在路径
file, _ := os.Open(path)
defer file.Close()
part, err := writer.CreateFormFile("content", filepath.Base(path))
if err != nil {
writer.Close()
t.Error(err)
}
io.Copy(part, file)
writer.Close()
myRouter := bootstrap.Bootstrap()
w := httptest.NewRecorder()
request, _ := http.NewRequest("POST", "/upload", body)
request.Header.Add("Content-Type", writer.FormDataContentType())
myRouter.ServeHTTP(w,request);
t.Log(w.Body.String())
}
upload.go
package controller
import (
"bufio"
"fmt"
"github.com/gin-gonic/gin"
"io"
"net/http"
)
func Upload(c *gin.Context) {
// 单文件
header, err := c.FormFile("content")
if err != nil {
errMsg := fmt.Sprintf("%s", err)
c.JSON(-1,errMsg)
return
}
fmt.Println(header.Filename)
fp,err := header.Open()
if err != nil {
errMsg := fmt.Sprintf("%s", err)
c.JSON(-1,errMsg)
return
}
defer fp.Close()
bufReader := bufio.NewReader(fp)
var lines [][]byte
for {
line, _, err := bufReader.ReadLine() // 按行读
if err != nil {
if err == io.EOF {
err = nil
break
}
} else {
lines = append(lines, line)
}
}
for i, line := range lines {
fmt.Printf("readfile: %d %s\n", i+1, line)
}
c.String(http.StatusOK, fmt.Sprintf("'%s' uploaded!", header.Filename))
}
package main
import (
"gotips/bootstrap"
)
main.go
func main() {
myRouter := bootstrap.Bootstrap()
myRouter.Run(":8080")
}
三、总结
golang 的高效便捷给开发人员带来了极大的便利,但是代码质量也不能忽视。单元测试也是一门技术,需要在开发过程中不断的总结和创新。除了阅读官方源码的单测例子,利用好google也能找到好多实用的单测技巧。希望在单测中遇到困难的留个言,共同完善golang的单元测试。
有疑问加站长微信联系(非本文作者)