Golang测试技术

标签: 技术志 Concurrency Go Golang GopherCon | 发表时间:2014-10-22 15:57 | 作者:bigwhite
出处:http://tonybai.com

本篇文章内容来源于 Golang核心开发组成员 Andrew Gerrand在Google I/O 2014的一次主题分享“ Testing Techniques”,即介绍使用Golang开发 时会使用到的测试技术(主要针对 单元测试),包括基本技术、高级技术(并发测试、 mock/fake、竞争条件测试、并发测试、内/外部测 试、vet工具等)等,感觉总结的很全面,这里整理记录下来,希望能给大家带来帮助。原Slide访问需要自己搭梯子。另外这里也要吐槽一 下:Golang官方站的slide都是以一种特有的golang artical的格式放出的,没法像pdf那样下载,在国内使用和传播极其不便。

一、基础测试技术

1、测试Go代码

Go语言内置测试框架。

内置的测试框架通过testing包以及go test命令来提供测试功能。

下面是一个完整的测试strings.Index函数的完整测试文件:

//strings_test.go (这里样例代码放入strings_test.go文件中)
package strings_test

import (
    "strings"
    "testing"
)

func TestIndex(t *testing.T) {
    const s, sep, want = "chicken", "ken", 4
    got := strings.Index(s, sep)
    if got != want {
        t.Errorf("Index(%q,%q) = %v; want %v", s, sep, got, want)// 注意原slide中 的got和want写反了
    }
}

$go test -v strings_test.go
=== RUN TestIndex
— PASS: TestIndex (0.00 seconds)
PASS
ok      command-line-arguments    0.007s

go test的-v选项是表示输出详细的执行信息。

将代码中的want常量值修改为3,我们制造一个无法通过的测试:

$go test -v strings_test.go
=== RUN TestIndex
— FAIL: TestIndex (0.00 seconds)
    strings_test.go:12: Index("chicken","ken") = 4; want 3
FAIL
exit status 1
FAIL    command-line-arguments    0.008s

2、表驱动测试

Golang的struct字面值(struct literals)语法让我们可以轻松写出表驱动测试。

package strings_test

import (
        "strings"
        "testing"
)

func TestIndex(t *testing.T) {
        var tests = []struct {
                s   string
                sep string
                out int
        }{
                {"", "", 0},
                {"", "a", -1},
                {"fo", "foo", -1},
                {"foo", "foo", 0},
                {"oofofoofooo", "f", 2},
                // etc
        }
        for _, test := range tests {
                actual := strings.Index(test.s, test.sep)
                if actual != test.out {
                        t.Errorf("Index(%q,%q) = %v; want %v",
                             test.s, test.sep, actual, test.out)
                }
        }
}

$go test -v strings_test.go
=== RUN TestIndex
— PASS: TestIndex (0.00 seconds)
PASS
ok      command-line-arguments    0.007s

3、T结构

*testing.T参数用于错误报告:

t.Errorf("got bar = %v, want %v", got, want)
t.Fatalf("Frobnicate(%v) returned error: %v", arg, err)
t.Logf("iteration %v", i)

也可以用于enable并行测试(parallet test):
t.Parallel()

控制一个测试是否运行:

if runtime.GOARCH == "arm" {
    t.Skip("this doesn't work on ARM")
}

4、运行测试

我们用go test命令来运行特定包的测试。

默认执行当前路径下包的测试代码。

$ go test
PASS

$ go test -v
=== RUN TestIndex
— PASS: TestIndex (0.00 seconds)
PASS

要运行工程下的所有测试,我们执行如下命令:

$ go test github.com/nf/…

标准库的测试:
$ go test std

注:假设strings_test.go的当前目录为testgo,在testgo目录下执行go test都是OK的。但如果我们切换到testgo的上一级目录执行go test,我们会得到什么结果呢?

$go test testgo
can't load package: package testgo: cannot find package "testgo" in any of:
    /usr/local/go/src/pkg/testgo (from $GOROOT)
    /Users/tony/Test/GoToolsProjects/src/testgo (from $GOPATH)

提示找不到testgo这个包,go test后面接着的应该是一个包名,go test会在GOROOT和GOPATH下查找这个包并执行包的测试。

5、测试覆盖率

go tool命令可以报告测试覆盖率统计。

我们在testgo下执行go test -cover,结果如下:

go build _/Users/tony/Test/Go/testgo: no buildable Go source files in /Users/tony/Test/Go/testgo
FAIL    _/Users/tony/Test/Go/testgo [build failed]

显然通过cover参数选项计算测试覆盖率不仅需要测试代码,还要有被测对象(一般是函数)的源码文件。

我们将目录切换到$GOROOT/src/pkg/strings下,执行go test -cover:

$go test -v -cover
=== RUN TestReader
— PASS: TestReader (0.00 seconds)
… …
=== RUN: ExampleTrimPrefix
— PASS: ExampleTrimPrefix (1.75us)
PASS
coverage: 96.9% of statements
ok      strings    0.612s

go test可以生成覆盖率的profile文件,这个文件可以被go tool cover工具解析。

在$GOROOT/src/pkg/strings下面执行:

$ go test -coverprofile=cover.out

会再当前目录下生成cover.out文件。

查看cover.out文件,有两种方法:

a) cover -func=cover.out

$sudo go tool cover -func=cover.out
strings/reader.go:24:    Len                66.7%
strings/reader.go:31:    Read                100.0%
strings/reader.go:44:    ReadAt                100.0%
strings/reader.go:59:    ReadByte            100.0%
strings/reader.go:69:    UnreadByte            100.0%
… …
strings/strings.go:638:    Replace                100.0%
strings/strings.go:674:    EqualFold            100.0%
total:            (statements)            96.9%

b) 可视化查看

执行go tool cover -html=cover.out命令,会在/tmp目录下生成目录coverxxxxxxx,比如/tmp/cover404256298。目录下有一个 coverage.html文件。用浏览器打开coverage.html,即可以可视化的查看代码的测试覆盖情况。

 

关于go tool的cover命令,我的go version go1.3 darwin/amd64默认并不自带,需要通过go get下载。

$sudo GOPATH=/Users/tony/Test/GoToolsProjects go get code.google.com/p/go.tools/cmd/cover

下载后,cover安装在$GOROOT/pkg/tool/darwin_amd64下面。

二、高级测试技术

1、一个例子程序

outyet是一个web服务,用于宣告某个特定Go版本是否已经打标签发布了。其获取方法:

go get github.com/golang/example/outyet

注:
go get执行后,cd $GOPATH/src/github.com/golang/example/outyet下,执行go run main.go。然后用浏览器打开http://localhost:8080即可访问该Web服务了。

2、测试Http客户端和服务端

net/http/httptest包提供了许多帮助函数,用于测试那些发送或处理Http请求的代码。

3、httptest.Server

httptest.Server在本地回环网口的一个系统选择的端口上listen。它常用于端到端的HTTP测试。

type Server struct {
    URL      string // base URL of form http://ipaddr:port with no trailing slash
    Listener net.Listener

    // TLS is the optional TLS configuration, populated with a new config
    // after TLS is started. If set on an unstarted server before StartTLS
    // is called, existing fields are copied into the new config.
    TLS *tls.Config

    // Config may be changed after calling NewUnstartedServer and
    // before Start or StartTLS.
    Config *http.Server
}

func NewServer(handler http.Handler) *Server

func (*Server) Close() error

4、httptest.Server实战

下面代码创建了一个临时Http Server,返回简单的Hello应答:

    ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        fmt.Fprintln(w, "Hello, client")
    }))
    defer ts.Close()

    res, err := http.Get(ts.URL)
    if err != nil {
        log.Fatal(err)
    }

    greeting, err := ioutil.ReadAll(res.Body)
    res.Body.Close()
    if err != nil {
        log.Fatal(err)
    }

    fmt.Printf("%s", greeting)

5、httptest.ResponseRecorder

httptest.ResponseRecorder是http.ResponseWriter的一个实现,用来记录变化,用在测试的后续检视中。

type ResponseRecorder struct {
    Code      int           // the HTTP response code from WriteHeader
    HeaderMap http.Header   // the HTTP response headers
    Body      *bytes.Buffer // if non-nil, the bytes.Buffer to append written data to
    Flushed   bool
}

6、httptest.ResponseRecorder实战

向一个HTTP handler中传入一个ResponseRecorder,通过它我们可以来检视生成的应答。

    handler := func(w http.ResponseWriter, r *http.Request) {
        http.Error(w, "something failed", http.StatusInternalServerError)
    }

    req, err := http.NewRequest("GET", "http://example.com/foo", nil)
    if err != nil {
        log.Fatal(err)
    }

    w := httptest.NewRecorder()
    handler(w, req)

    fmt.Printf("%d – %s", w.Code, w.Body.String())

7、竞争检测(race detection)

当两个goroutine并发访问同一个变量,且至少一个goroutine对变量进行写操作时,就会发生数据竞争(data race)。

为了协助诊断这种bug,Go提供了一个内置的数据竞争检测工具。

通过传入-race选项,go tool就可以启动竞争检测。

$ go test -race mypkg    // to test the package
$ go run -race mysrc.go  // to run the source file
$ go build -race mycmd   // to build the command
$ go install -race mypkg // to install the package

注:一个数据竞争检测的例子

例子代码:

//testrace.go

package main

import "fmt"
import "time"

func main() {
        var i int = 0
        go func() {
                for {
                        i++
                        fmt.Println("subroutine: i = ", i)
                        time.Sleep(1 * time.Second)
                }
        }()

        for {
                i++
                fmt.Println("mainroutine: i = ", i)
                time.Sleep(1 * time.Second)
        }
}

$go run -race testrace.go
mainroutine: i =  1
==================
WARNING: DATA RACE
Read by goroutine 5:
  main.func·001()
      /Users/tony/Test/Go/testrace.go:10 +0×49

Previous write by main goroutine:
  main.main()
      /Users/tony/Test/Go/testrace.go:17 +0xd5

Goroutine 5 (running) created at:
  main.main()
      /Users/tony/Test/Go/testrace.go:14 +0xaf
==================
subroutine: i =  2
mainroutine: i =  3
subroutine: i =  4
mainroutine: i =  5
subroutine: i =  6
mainroutine: i =  7
subroutine: i =  8

8、测试并发 (testing with concurrency)

当测试并发代码时,总会有一种使用sleep的冲动。大多时间里,使用sleep既简单又有效。

但大多数时间不是”总是“。

我们可以使用Go的并发原语让那些奇怪不靠谱的sleep驱动的测试更加值得信赖。

9、 使用静态分析工具vet查找错误

vet工具用于检测代码中程序员犯的常见错误:
    – 错误的printf格式
    – 错误的构建tag
    – 在闭包中使用错误的range循环变量
    – 无用的赋值操作
    – 无法到达的代码
    – 错误使用mutex
    等等。

使用方法:
    go vet [package]

10、 从内部测试

golang中大多数测试代码都是被测试包的源码的一部分。这意味着测试代码可以访问包种未导出的符号以及内部逻辑。就像我们之前看到的那样。

注:比如$GOROOT/src/pkg/path/path_test.go与path.go都在path这个包下。

11、从外部测试

有些时候,你需要从被测包的外部对被测包进行测试,比如测试代码在package foo_test下,而不是在package foo下。

这样可以打破依赖循环,比如:

    – testing包使用fmt
    – fmt包的测试代码还必须导入testing包
    – 于是,fmt包的测试代码放在fmt_test包下,这样既可以导入testing包,也可以同时导入fmt包。

12、Mocks和fakes

通过在代码中使用interface,Go可以避免使用mock和fake测试机制。

例如,如果你正在编写一个文件格式解析器,不要这样设计函数:

func Parser(f *os.File) error

作为替代,你可以编写一个接受interface类型的函数:

func Parser(r io.Reader) error

和bytes.Buffer、strings.Reader一样,*os.File也实现了io.Reader接口。

13、子进程测试

有些时候,你需要测试的是一个进程的行为,而不仅仅是一个函数。例如:

func Crasher() {
    fmt.Println("Going down in flames!")
    os.Exit(1)
}

为了测试上面的代码,我们将测试程序本身作为一个子进程进行测试:

func TestCrasher(t *testing.T) {
    if os.Getenv("BE_CRASHER") == "1" {
        Crasher()
        return
    }
    cmd := exec.Command(os.Args[0], "-test.run=TestCrasher")
    cmd.Env = append(os.Environ(), "BE_CRASHER=1")
    err := cmd.Run()
    if e, ok := err.(*exec.ExitError); ok && !e.Success() {
        return
    }
    t.Fatalf("process ran with err %v, want exit status 1", err)
}

© 2014, bigwhite. 版权所有.

相关 [golang 测试 技术] 推荐:

Golang测试技术

- - Tony Bai
本篇文章内容来源于 Golang核心开发组成员 Andrew Gerrand在Google I/O 2014的一次主题分享“ Testing Techniques”,即介绍使用Golang开发 时会使用到的测试技术(主要针对. 单元测试),包括基本技术、高级技术(并发测试、 mock/fake、竞争条件测试、并发测试、内/外部测 试、vet工具等)等,感觉总结的很全面,这里整理记录下来,希望能给大家带来帮助.

golang的杀手级应用:docker

- - _不是我干的 _
docker 是 golang 的第一个杀手级应用,发展迅猛, 现在各大云计算平台几乎全都支持 docker 实例,包括 谷歌,亚马逊,阿里云等. golang 本身已经让我惊喜万分,而 docker 更是极大的激发了我对虚拟化的想象. IT 业发展至今,软件和硬件始终是无法分割的两个物体. 就拿最近几年红红火火的智能机时代来说, 很久之前的诺基亚智能机, 软件和硬件相辅相成, 连进入主界面都需要按一个特定的按钮才能进入.

Golang 大杀器之跟踪剖析 trace

- - SegmentFault 最新的文章
在 Go 中有许许多多的分析工具,在之前我有写过一篇 《Golang 大杀器之性能剖析 PProf》 来介绍 PProf,如果有小伙伴感兴趣可以去我博客看看. 但单单使用 PProf 有时候不一定足够完整,因为在真实的程序中还包含许多的隐藏动作,例如 Goroutine 在执行时会做哪些操作. GC 是怎么影响到 Goroutine 的执行的.

基于Golang的微服务——Micro实践

- - IT瘾-tuicool
开始开发前需要先配置好Go的开发环境,可以看我写的 基于Golang的微服务——上手篇. 在 GOPATH目录下的src目录下创建我们的实战项目目录 tech,切换到这个目录. go get github.com/micro/go-micro //用于开发的微服务的RPC框架,是micro架构的基础 go get github.com/micro/protoc-gen-micro // 用于生成Protobuf的代码 go get github.com/micro/micro // 工具集安装,会自动将 micro加入环境变量 复制代码.

springboot单元测试技术

- - 海思
整个软件交付过程中,单元测试阶段是一个能够最早发现问题,并且可以重复回归问题的阶段,在单元测试阶段做的测试越充分,软件质量就越能得到保证. 具体的代码参照 示例项目 https://github.com/qihaiyan/springcamp/tree/master/spring-unit-test.

gocode——VIM 和 Emacs 的 golang 代码自动补全

- XiaoHui - Some reminiscences, some memories
虽然 golang 自身提供了 VIM 的语法高亮之类的脚本,但 autocompletion 并没有官方解决方案. 无意之中发现 gocode 这个支持 VIM 和 Emacs 的 autocompletion daemon. 这里有个Flash 动画演示,展示了 gocode 的强大. 我得说,用过之后,感觉速度确实够快.

windows 下搭建 GoLang 语言开发环境

- - haohtml's blog
golang官方二进制分发包包括FreeBSD, Linux, Mac OS X (Snow Leopard/Lion), and Windows等平台,包括32位、64位等版本. 我自己使用的是windows 32位分发包,MSI格式的,下载地址为: http://code.google.com/p/go/downloads/list.

[转][转]Golang适合高并发场景的原因分析

- - heiyeluren的blog(黑夜路人的开源世界)
来源: http://blog.csdn.net/ghj1976/article/details/27996095. 我们先看两个用Go做消息推送的案例实际处理能力. 16台机器,标配:24个硬件线程,64GB内存 . 单机80万并发连接,load 0.2~0.4,CPU 总使用率 7%~10%,内存占用20GB (res) .

Golang并发模型:轻松入门流水线FAN模式

- - SegmentFault 最新的文章
前一篇文章 《Golang并发模型:轻松入门流水线模型》,介绍了流水线模型的概念,这篇文章是流水线模型进阶,介绍FAN-IN和FAN-OUT,FAN模式可以让我们的流水线模型更好的利用Golang并发,提高软件性能. 但FAN模式不一定是万能,不见得能提高程序的性能,甚至还不如普通的流水线. 我们先介绍下FAN模式,再看看它怎么提升性能的,它是不是万能的.

基于 HLS 创建 Golang 视频流服务器

- - 掘金 前端
HLS 是 HTTP Live Streaming 的缩写,是苹果开发的一种基于 HTTP 的自适应比特率流媒体传输协议, 并于 2009 年. HLS 流媒体已经成为应用最广泛的实时视频协议. 它是一种将流分解成基于文件小段的格式, 可以通过 HTTP 下载,HLS 可以通过标准的 HTTP 或代理服务器等,这和基于 UDP 的协议(例如 RTP)不同.