内容简介:关于为什么要单元测试,记得有的人说过,从单元测试,到业务测试再到UI测试,越底层发现错误越快,修改的成本也越低。就自己来说,最近用到了golang的项目,发现 golang 的单元你测试不如 java 的 springboot的好用,因此做了个技巧的总结,希望能方便单元测试。有时我们的代码依赖外部组件,但是外部组件无法提供单测环境,或者按正常流程运行不起来,这个时候就可以考虑用mock的方式处理,专注于自己模块的测试。使用的组件:
一、 背景
关于为什么要单元测试,记得有的人说过,从单元测试,到业务测试再到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 可以设置运行的测试模板,而且使用成本很低,因此采用这种方式传参。设置方式如下图:
Jietu20190616-130243.jpg
读取参数例子
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的单元测试。
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网
猜你喜欢:- go test单元测试技巧
- 写好单元测试的10个技巧
- 学习 Node.js,第 9 单元:单元测试
- Vue 应用单元测试的策略与实践 02 - 单元测试基础
- Vue 应用单元测试的策略与实践 04 - Vuex 单元测试
- Vue 应用单元测试的策略与实践 03 - Vue 组件单元测试
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
首席产品官2 从白领到金领
车马 / 机械工业出版社 / 79元
《首席产品官》共2册,旨在为产品新人成长为产品行家,产品白领成长为产品金领,最后成长为首席产品官(CPO)提供产品认知、能力体系、成长方法三个维度的全方位指导。 作者在互联网领域从业近20年,是中国早期的互联网产品经理,曾是周鸿祎旗下“3721”的产品经理,担任CPO和CEO多年。作者将自己多年来的产品经验体系化,锤炼出了“产品人的能力杠铃模型”(简称“杠铃模型”),简洁、直观、兼容性好、实......一起来看看 《首席产品官2 从白领到金领》 这本书的介绍吧!