内容简介:关于为什么要单元测试,记得有的人说过,从单元测试,到业务测试再到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 组件单元测试
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
Beautiful Code
Greg Wilson、Andy Oram / O'Reilly Media / 2007-7-6 / GBP 35.99
In this unique work, leading computer scientists discuss how they found unusual, carefully designed solutions to difficult problems. This book lets the reader look over the shoulder of major coding an......一起来看看 《Beautiful Code》 这本书的介绍吧!
RGB HSV 转换
RGB HSV 互转工具
HEX CMYK 转换工具
HEX CMYK 互转工具