golang 单元测试几个小技巧

BigFish_大鱼 · · 2635 次点击 · · 开始浏览    
这是一个创建于 的文章,其中的信息可能已经有所发展或是发生改变。

一、 背景

关于为什么要单元测试,记得有的人说过,从单元测试,到业务测试再到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的单元测试。


有疑问加站长微信联系(非本文作者)

本文来自:简书

感谢作者:BigFish_大鱼

查看原文:golang 单元测试几个小技巧

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

2635 次点击  
加入收藏 微博
暂无回复
添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传