[译]从正确的方式开始测试你的Go应用

从正确的方式开始测试你的Go应用

/**
 * 谨献给Yoyo
 *
 * 原文出处:https://www.toptal.com/go/your-introductory-course-to-testing-with-go
 * @author dogstar.huang <chanzonghuang@gmail.com> 2016-08-11
 */

在学习任何新的东西时,具有清醒的头脑是很重要的。html

若是你对Go至关陌生,并来自诸如JavaScript或Ruby这样的语言,你极可能习惯于使用现成的框架来帮助你模拟、断言以及作一些其余测试的 您嘲笑,断言,和作其余测试巫术。git

如今,消除基于于外部依赖或框架的想法!几年前在学习这门出众的编程语言时,测试是我遇到的第一个障碍,那时只有至关少的一些资源可用。github

如今我知道了,在GO中测试成功,意味着对依赖轻装上阵(如同和GO全部事情那样),最少依赖于外部类库,以及编写更好、可重用的代码。此Blake Mizerany的经验介绍勇于向第三方测试库尝试,是一个调整你思想很好的开始。你将看到一些关于使用外部类库以及“Go的方式”的框架的争论。golang

想学Go吗?来看看咱们Golang的入门教程吧。编程

构建本身的测试框架和模拟的概念彷佛违反直觉,但也能够很容易就能想到,对于学习这门语言,这是一个良好的起点。另外,不像我当时学习那样,在贯穿常见测试脚本以及介绍我认为是有效测试和保持代码整洁最佳实践的过程当中,你都有这篇文章做为引导。json

以“Go的方式”来作事,消除对外部框架的依赖。安全

Go中的表格测试

基本的测试单元 - “单元测试”的名声 - 能够是一个程序的任何部分,它以最简单的形式,只须要一个输入并返回一个输出。让咱们来看一个将要为其编写测试的简单函数。显然,它远不是完美和完成的,但出于演示目的是足够好的了:
avg.go服务器

func Avg(nos ...int) int {  
    sum := 0
    for _, n := range nos {
        sum += n
    }
    if sum == 0 {
        return 0
    }
    return sum / len(nos)
}

上面函数,func Avg(nos ...int),返回零或给它一系列数字的整数平均值。如今让咱们来给它写一个测试吧。架构

在Go中,给测试文件命名和包含待测试代码的文件相同名称,并带上附加的后缀_test被当为最佳实践。例如,上面代码是一个名为avg.go的文件中,因此咱们的测试文件将被命名为avg_test.goapp

注意,这些示例只是实际文件的摘录,由于包定义和导入出于简化已删去。

这是针对Avg函数的测试:

avg_test.go

func TestAvg(t *testing.T) {  
    for _, tt := range []struct {
        Nos    []int
        Result int
    }{
        {Nos: []int{2, 4}, Result: 3},
        {Nos: []int{1, 2, 5}, Result: 2},
        {Nos: []int{1}, Result: 1},
        {Nos: []int{}, Result: 0},
        {Nos: []int{2, -2}, Result: 0},
    } {
        if avg := Average(tt.Nos...); avg != tt.Result {
            t.Fatalf("expected average of %v to be %d, got %d\n", tt.Nos, tt.Result, avg)
        }
    }
}

关于函数定义,有几件事情须要注意的:

  • 首先,在测试函数名称的“Test”前缀。这是必需的,以便工具把它做为一种有效的测试检测出来。
  • 测试函数名称的后半部分一般是待测试函数或者方法的名称,在这里是Avg
  • 咱们还须要传入称为testing.T的测试结构,其容许控制测试流。有关此API的更多详细信息,请访问此文档

如今,让咱们来聊聊这个例子编写的格式。一个测试套件(一系列测试)正经过Agv()函数运行,而且每一个测试含一个特定的输入和预期的输出。在咱们的例子中,每次测试传入一系列整数(Nos)和所指望的一个特定的返回值(Result)。

表格测试从它的结构得名,很容易被表示成一个有两列的表格:输入变量和预期的输出变量。

Golang接口模拟

Go语言所提供的最伟大和最强大的功能称为接口。除了在进行程序架构设计时得到接口的强大功能和灵活性外,接口也为咱们提供了使人惊讶的机会来解耦组件以及在交汇点全面测试他们。

接口是指定方法的集合,也是一个变量类型。

让咱们看一个虚构的场景,假设须要从io.Reader读取前N个字节,并把它们做为一个字符串返回。它看起来像是这样:
readn.go

// readN reads at most n bytes from r and returns them as a string.
func readN(r io.Reader, n int) (string, error) {  
    buf := make([]byte, n)
    m, err := r.Read(buf)
    if err != nil {
        return "", err
    }
    return string(buf[:m]), nil
}


显然,主要要测试的是readN这个功能,当给定各类输入时,返回正确的输出。这能够用表格测试来完成。但另外也有两个特殊的场景该覆盖到,那就是要检查:

  • readN被一个大小为n的缓冲调用
  • readN返回一个错误若是抛出异常

为了知道传递给r.Read的缓冲区的大小,以及控制它返回的错误,咱们须要模拟传递给readNr。若是看一下Go文档中的Reader类型,咱们看到io.Reader看起来像:

type Reader interface {  
       Read(p []byte) (n int, err error)
}

这彷佛至关容易。为了知足io.Reader咱们所须要作是有自已模拟一个Read方法。因此,ReaderMock能够是这样:

type ReaderMock struct {  
    ReadMock func([]byte) (int, error)
}

func (m ReaderMock) Read(p []byte) (int, error) {  
    return m.ReadMock(p)
}

咱们稍微来分析一下上面的代码。任何ReaderMock的实例明显地知足了io.Reader接口,由于它实现了必要的Read方法。咱们的模拟还包含字段ReadMock,使咱们可以设置模拟方法确切的行为,这使得动态实例任何须要的行为很是容易。

为确保接口在运行时能知足须要,一个伟大的不消耗内存的技巧是,把如下代码插入到咱们的代码中:

var _ io.Reader = (*MockReader)(nil)

这样会检查断言,但不会分配任何东西,这让咱们确保该接口在编译时已被正确实现,即在该程序真正使用它运行到任何功能以前。可选的技巧,但很实用。

继续往前,让咱们来写第一个测试:r.Read被大小为n的缓冲区调用。为了作到这点,咱们傅用了ReaderMock,以下:

func TestReadN_bufSize(t *testing.T) {  
    total := 0
    mr := &MockReader{func(b []byte) (int, error) {
        total = len(b)
        return 0, nil
    }}
    readN(mr, 5)
    if total != 5 {
        t.Fatalf("expected 5, got %d", total)
    }
}

正如你在上面看到的,咱们经过一个局部变量定义了“假的”io.Reader功能,这可用于后面断言咱们的测试的有效性。至关容易。

再来看下须要测试的第二个场景,这要求咱们模拟Read以返回一个错误:

func TestReadN_error(t *testing.T) {  
    expect := errors.New("some non-nil error")
    mr := &MockReader{func(b []byte) (int, error) {
        return 0, expect
    }}
    _, err := readN(mr, 5)
    if err != expect {
        t.Fatal("expected error")
    }
}

在上面的测试,无论什么调用了mr.Read(咱们模拟的Reader)都将返回既定义的错误,所以假设readN的正常运行也会这样作是可靠的。

Golang方法模拟

一般咱们不须要模拟方法,由于取而代之,咱们倾向于使用结构和接口。这些更容易控制,但偶尔会碰到这种必要性,我常常看到围绕这块话题的困惑。甚至有人问怎么模拟相似log.Println这样的东西。虽然不多须要测试给log.Println的输入的状况,咱们将利用此次机会来证实。

考虑如下简单的if语句,根据n的值输出记录:

func printSize(n int) {  
    if n < 10 {
        log.Println("SMALL")
    } else {
        log.Println("LARGE")
    }
}

在上面的例子中,咱们假设这样一个好笑的场景:特定测试log.Println被正确的值调用。为了模拟这个功能,首先须要把它包装起来:

var show = func(v ...interface{}) {  
    log.Println(v...)
}

以这种方式声方法 - 做为一个变量 - 容许咱们在测试中覆盖它,并为其分配任何咱们所但愿的行为。间接地,把log.Println的代码行替换成show,那么咱们的程序将变成:

func printSize(n int) {  
    if n < 10 {
        show("SMALL")
    } else {
        show("LARGE")
    }
}

如今咱们能够测试了:

func TestPrintSize(t *testing.T) {  
    var got string
    oldShow := show
    show = func(v ...interface{}) {
        if len(v) != 1 {
            t.Fatalf("expected show to be called with 1 param, got %d", len(v))
        }
        var ok bool
        got, ok = v[0].(string)
        if !ok {
            t.Fatal("expected show to be called with a string")
        }
    }

    for _, tt := range []struct{
        N int
        Out string
    }{
        {2, "SMALL"},
        {3, "SMALL"},
        {9, "SMALL"},
        {10, "LARGE"},
        {11, "LARGE"},
        {100, "LARGE"},
    } {
        got = ""
        printSize(tt.N)
        if got != tt.Out {
            t.Fatalf("on %d, expected '%s', got '%s'\n", tt.N, tt.Out, got)
        }
    }

    // careful though, we must not forget to restore it to its original value
    // before finishing the test, or it might interfere with other tests in our
    // suite, giving us unexpected and hard to trace behavior.
    show = oldShow
}

咱们不该该“模拟log.Println”,但在那些很是偶然的状况下,当咱们出于正当理由真的须要模拟一个包级的方法时,为了作到这一点惟一的方法(据我所知)是把它声明为一个包级的变量,这样咱们就能够控制它的值。

然而,若是咱们确实须要模拟像log.Println这样的东西,假如使用了自定义的记录器,咱们能够编写一个更优雅的解决方案。

Golang模板渲染测试

另外一个至关常见的状况是,根据预期测试某个渲染模板的输出。让咱们考虑一个对http://localhost:3999/welcome?name=Frank的GET请求,它会返回如下body:

<html>  
    <head><title>Welcome page</title></head>
    <body>
        <h1 class="header-name">
            Welcome <span class="name">Frank</span>!
        </h1>
    </body>
</html>

若是如今它明显不够,查询参数name与类为namespan标签相匹配,这不是一个巧合。在这种状况下,明显的测试应该验证每次跨越多层输出时这种状况都正确发生。在这里我发现GoQuery类库很是有用。

GoQuery使用相似jQuery的API查询HTML结构,是用于测试程序标签输出的有效性是必不可少的。

如今用这种方式咱们能够编写咱们的测试了:
welcome__test.go

func TestWelcome_name(t *testing.T) {  
    resp, err := http.Get("http://localhost:3999/welcome?name=Frank")
    if err != nil {
        t.Fatal(err)
    }
    if resp.StatusCode != http.StatusOK {
        t.Fatalf("expected 200, got %d", resp.StatusCode)
    }
    doc, err := goquery.NewDocumentFromResponse(resp)
    if err != nil {
        t.Fatal(err)
    }
    if v := doc.Find("h1.header-name span.name").Text(); v != "Frank" {
        t.Fatalf("expected markup to contain 'Frank', got '%s'", v)
    }
}

首先,在处理前咱们检查响应状态码是否是200/OK。

我认为,假设上面的代码段的其他部分是不言自明不会太牵强:咱们使用http包来提取URL并根据响应建立一个新的goquery兼容文档,随后咱们会用它来查询返回的DOM。咱们检查了在h1.header-name里面span.name封装文本'弗兰克'。

测试JSON接口

Golang常常用来写某种API,因此最后但并不是最不重要的,让咱们来看一些测试JSON API高级的方式。

试想,若是前面的终端返回的是JSON而不是HTML,那么从http://localhost:3999/welcome.json?name=Frank咱们将期待响应的body看起来像这样:

{"Salutation": "Hello Frank!"}

断言JSON响应,正如你能想到那样,相比于断言模板响应来讲并不会有太大的不一样,有一点特殊就是咱们不须要任何外部库或依赖。 Golang的标准库就足够了。下面是咱们的测试,以确认对于给定的参数返回正确的JSON:
welcome__test.go

func TestWelcome_name_JSON(t *testing.T) {  
    resp, err := http.Get("http://localhost:3999/welcome.json?name=Frank")
    if err != nil {
        t.Fatal(err)
    }
    if resp.StatusCode != 200 {
        t.Fatalf("expected 200, got %d", resp.StatusCode)
    }
    var dst struct{ Salutation string }
    if err := json.NewDecoder(resp.Body).Decode(&dst); err != nil {
        t.Fatal(err)
    }
    if dst.Salutation != "Hello Frank!" {
        t.Fatalf("expected 'Hello Frank!', got '%s'", dst.Salutation)
    }
}

若是返回了解码之外的任何结构,json.NewDecoder反而会返回一个错误,测试将会失败。考虑到成功对响应结构解码后,咱们检查字段的内容是否达到预期 - 在咱们的状况下是“Hello Frank!”。

Setup 和 Teardown

Golang的测试是容易的,但有一个问题,在这以前的JSON测试和模板渲染测试。他们都假设服务器正在运行,而这建立了一个不可靠的依赖。而且,须要一个“活”的服务器不是一个很好的主意。

测试在一个“活”的生产服务器上的“实时”数据历来都不是一个好主意;从本地自旋而上或开发副本,因此没有事情作了后会有严重的损害。

幸运的是,Golang提供了httptest包来建立测试服务器。测试引起了本身独立的服务器,独立于咱们主要的服务器,所以测试不会干扰生产环境。

在这种状况下,建立通用的setupteardown方法以便被所有须要运行服务器的测试调用是理想的。根据这一新的、更安全的模式,咱们的测试最终看起来像这样:

func setup() *httptest.Server {  
    return httptest.NewServer(app.Handler())
}

func teardown(s *httptest.Server) {  
    s.Close()
}

func TestWelcome_name(t *testing.T) {  
    srv := setup()

    url := fmt.Sprintf("%s/welcome.json?name=Frank", srv.URL)
    resp, err := http.Get(url)
    // verify errors & run assertions as usual

    teardown(srv)
}

注意app.Handler()的引用。这是一个最佳实践的函数,它返回了应用程序的http Handler,它既能够实例化生产服务器也能够实例化测试服务器。

结论

Golang的测试是一个很好的机会,它假定了你程序的外部视野和承担访问者的脚步,或是在大多数状况下,即你的API的用户。它提供了巨大的机会,以确保你提供了良好的代码和优质的体验。

无论什么时候当你不肯定代码中更为复杂的功能时,测试做为一颗定心丸就会派上用场,同时也保证了当修改较大系统的组成部分时其余部分仍能继续一块儿很好工做。

但愿这篇文章能对你有用,若是您知道任何其余测试技巧也欢迎来发表评论。


------------------------

相关文章
相关标签/搜索