zoukankan      html  css  js  c++  java
  • Golang测试技术

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

     

  • 相关阅读:
    GeoServer 2.2 正式版发布,GIS 服务器
    Spring Shell 1.0.0.RC1 发布
    微软发布支持Windows Desktop和F#的Visual Studio Express版本
    XINS 3.0 正式版发布,远程 API 调用规范
    YUI 3.7.2 补丁版发布
    HTML5 Boilerplate 4:改进了Apache配置和图片替换技术,并采用MIT许可证
    解决Windows Phone平台上不能枚举工程自带资源的问题
    截短 UTF8 字符串
    Spring Data Neo4j 2.1.0 RC4 发布
    GTK+ 3.5.18 发布,GUI 开发工具包
  • 原文地址:https://www.cnblogs.com/davygeek/p/5164687.html
Copyright © 2011-2022 走看看