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)
    }

     

  • 相关阅读:
    poj 1579(动态规划初探之记忆化搜索)
    hdu 1133(卡特兰数变形)
    CodeForces 625A Guest From the Past
    CodeForces 625D Finals in arithmetic
    CDOJ 1268 Open the lightings
    HDU 4008 Parent and son
    HDU 4044 GeoDefense
    HDU 4169 UVALive 5741 Wealthy Family
    HDU 3452 Bonsai
    HDU 3586 Information Disturbing
  • 原文地址:https://www.cnblogs.com/davygeek/p/5164687.html
Copyright © 2011-2022 走看看