go语言——测试

栏目: Go · 发布时间: 5年前

内容简介:测试的目的是确认目标代码在给定的场景下,有没有按照期望工作 。一个场景是正向路经测试,就是在正常执行的情况下,保证代码不产生错误的测试。另外一些单元测试可能会测试负向路径的场景,保证代码不仅会产生错误,而且是预期的错误。总之,不管如何调用或者执行代码,所写的代码行为都是可预期的go语言为我们提供了测试框架testing和自带go test命令来实现单元测试和性能测试。

go语言测试

测试的目的是确认目标代码在给定的场景下,有没有按照期望工作 。一个场景是正向路经测试,就是在正常执行的情况下,保证代码不产生错误的测试。

另外一些单元测试可能会测试负向路径的场景,保证代码不仅会产生错误,而且是预期的错误。总之,不管如何调用或者执行代码,所写的代码行为都是可预期的

go语言为我们提供了测试框架testing和自带go test命令来实现单元测试和性能测试。

1、单元测试

go语言自带的testing 框架使用:

func TestAdd(t *testing.T)()

1.1 基础单元测试

比如我们有一个函数Sum(a,b int) int 需要测试,

main.go

package main

func  Sum(a,b int )int{
    return  a+b
}

main_test.go

package main

import (
    "testing"
)

func TestSum(t *testing.T){
    sum := Sum(1, 2)
    if sum==3{
        t.Logf("成功,结果为%v",sum)
    }else{
        t.Fatalf("有错误,结果为%v",sum)
    }
}

运行测试命令;

//命令行执行 go test 命令
PASS
ok      _/D_/gopath/src/a_tour_of_go/testing    1.301s
//执行go test -v 命令
=== RUN   TestSum
--- PASS: TestSum (0.00s)
    main_test.go:10: 成功,结果为3
PASS
ok      _/D_/gopath/src/a_tour_of_go/testing    1.302s
//执行 go test -v main_test.go main.go
结果同上,因为我们就写了一个测试文件。
//执行 go test -v -test.run TestSum
结果同上,因为只有一个函数

其中,*testing.T 有一些方法方便使用

type T
    func (c *T) Error(args ...interface{})
    func (c *T) Errorf(format string, args ...interface{})
    func (c *T) Fail()
    func (c *T) FailNow()
    func (c *T) Failed() bool
    func (c *T) Fatal(args ...interface{})
    func (c *T) Fatalf(format string, args ...interface{})
    func (c *T) Helper()
    func (c *T) Log(args ...interface{})
    func (c *T) Logf(format string, args ...interface{})
    func (c *T) Name() string
    func (t *T) Parallel()
    func (t *T) Run(name string, f func(t *T)) bool
    func (c *T) Skip(args ...interface{})
    func (c *T) SkipNow()
    func (c *T) Skipf(format string, args ...interface{})
    func (c *T) Skipped() bool

比如我们常用,Fatalf() 出现错误是调用会打印,并且结束函数, Logf() 打印自定义信息。

1.2 表组测试

所谓的表组测试,基本上和单元测试一样,只不过它有好几个不同输入以及输出组成的一组单元测试。

简单修改:无非就是多测几次不同的值呗

package main

import (
    "testing"
)

func TestSum(t *testing.T){
    sum := Sum(1, 2)
    if sum==3{
        t.Logf("成功,结果为%v",sum)
    }else{
        t.Fatalf("有错误,结果为%v",sum)
    }
    sum1 := Sum(4, 4)
    if sum1==8{
        t.Logf("成功,结果为%v",sum1)
    }else{
        t.Fatalf("有错误,结果为%v",sum1)
    }
    sum2 := Sum(5, 5)
    if sum2==10{
        t.Logf("成功,结果为%v",sum2)
    }else{
        t.Fatalf("有错误,结果为%v",sum2)
    }
}
//结果
=== RUN   TestSum
--- PASS: TestSum (0.00s)
    main_test.go:10: 成功,结果为3
    main_test.go:16: 成功,结果为8
    main_test.go:22: 成功,结果为10
PASS
ok      a_tour_of_go/testing    1.253s

1.3 模仿调用

当我们测试需要网络访问时,我们并没有联网,又不能时时开启服务器,所以这时候模拟网络访问就有必要了。

针对模拟网络访问,标准库了提供了一个 httptest 包,可以让我们模拟http的网络调用。

首先我们创建一个处理HTTP请求的函数,并注册路由

package common

import (
    "net/http"
    "encoding/json"
)

func Routes(){
    http.HandleFunc("/sendjson",SendJSON)
}

func SendJSON(rw http.ResponseWriter,r *http.Request){
    u := struct {
        Name string
    }{
        Name:"张三",
    }

    rw.Header().Set("Content-Type","application/json")
    rw.WriteHeader(http.StatusOK)
    json.NewEncoder(rw).Encode(u)
}

非常简单,这里是一个 /sendjson API,当我们访问这个API时,会返回一个JSON字符串。现在我们对这个API服务进行测试,但是我们又不能时时刻刻都启动着服务,所以这里就用到了外部终端对API的网络访问请求。

func init()  {
    common.Routes()
}

func TestSendJSON(t *testing.T){
    req,err:=http.NewRequest(http.MethodGet,"/sendjson",nil)
    if err!=nil {
        t.Fatal("创建Request失败")
    }

    rw:=httptest.NewRecorder()
    http.DefaultServeMux.ServeHTTP(rw,req)

    log.Println("code:",rw.Code)

    log.Println("body:",rw.Body.String())
}

运行这个单元测试,就可以看到我们访问 /sendjson API的结果里,并且我们没有启动任何HTTP服务就达到了目的。这个主要利用 httptest.NewRecorder() 创建一个 http.ResponseWriter ,模拟了真实服务端的响应,这种响应时通过调用 http.DefaultServeMux.ServeHTTP 方法触发的。

还有一个模拟调用的方式,是真的在测试机上模拟一个服务器,然后进行调用测试。

func mockServer() *httptest.Server {
    //API调用处理函数
    sendJson := func(rw http.ResponseWriter, r *http.Request) {
        u := struct {
            Name string
        }{
            Name: "张三",
        }

        rw.Header().Set("Content-Type", "application/json")
        rw.WriteHeader(http.StatusOK)
        json.NewEncoder(rw).Encode(u)
    }
    //适配器转换
    return httptest.NewServer(http.HandlerFunc(sendJson))
}

func TestSendJSON(t *testing.T) {
    //创建一个模拟的服务器
    server := mockServer()
    defer server.Close()
    //Get请求发往模拟服务器的地址
    resq, err := http.Get(server.URL)
    if err != nil {
        t.Fatal("创建Get失败")
    }
    defer resq.Body.Close()

    log.Println("code:", resq.StatusCode)
    json, err := ioutil.ReadAll(resq.Body)
    if err != nil {
        log.Fatal(err)
    }
    log.Printf("body:%s\n", json)
}

模拟服务器的创建使用的是 httptest.NewServer 函数,它接收一个 http.Handler 处理API请求的接口。 代码示例中使用了Hander的适配器模式, http.HandlerFunc 是一个函数类型,实现了 http.Handler 接口,这里是强制类型转换,不是函数的调用。

这个创建的模拟服务器,监听的是本机IP 127.0.0.1 ,端口是随机的。接着我们发送Get请求的时候,不再发往 /sendjson ,而是模拟服务器的地址 server.URL ,剩下的就和访问正常的URL一样了,打印出结果即可。

1.4 测试覆盖率

就其性质而言,测试不可能是完整的 。再多测试也不能说明程序没有bug,测试可以增强我们的信心,让我们的程序在一个放心的环境中正常运行。

由单元测试的代码,触发运行到的被测试代码的代码行数占所有代码行数的比例,被称为测试覆盖率,代码覆盖率不一定完全精准,但是可以作为参考,可以帮我们测量和我们预计的覆盖率之间的差距, go test 工具,就为我们提供了这么一个度量测试覆盖率的能力。

简单来说就是一个参数 - coverprofile

比如:

main.go

package main

import "fmt"

func Tag(tag int){
    switch tag {
    case 1:
        fmt.Println("Android")
    case 2:
        fmt.Println("Go")
    case 3:
        fmt.Println("Java")
    default:
        fmt.Println("C")

    }
}

main_test.go

package main

import (
    "testing"
)

func TestTag(t *testing.T) {
    Tag(1)
    Tag(2)

}

执行命令: go test -v -coverprofile=c.out 输出结果:

=== RUN TestTag

Android

Go

--- PASS: TestTag (0.00s)

PASS

coverage: 60.0% of statements

ok a_tour_of_go/testing 1.309s

得到测试覆盖率为60% , 我们之前的c.out 是生成的测试报告,我们可以看到当前目录下有一个c.out文件

我们可以生成 html 文件 go tool cover -html=c.out 会直接打开一个网页,显示我们的代码,通过颜色区分。 当然也可以 go tool cover -html=c.out -o=tag.html 在当前目录生成一个名为tag.html的文件,双击打开,一样的。

go语言——测试

标记为绿色的代码行已经被测试了;标记为红色的还没有测试到。我们根据没有测试到的代码逻辑,完善我的单元测试代码即可。

2.基准测试

2.1 进行基准测试

基准测试是一种测试代码性能的方法。想要测试解决同一问题的不同方案的性能,以及查看哪种解决方案的性能更好时,基准测试就会很有用。

基准测试也可以用来识别某段代码的 CPU或者内存效率问题,而这段代码的效率可能会严重影响整个应用程序的性能。许多开发人员会用基准测试来测试不同的并发模式,或者用基准测试来辅助配置工作池的数量,以保证能最大化系统的吞吐量。

main_test.go

package main

import (
    "fmt"
    "testing"
)

func BenchmarkSprintf(b *testing.B){
    num:=10
    b.ResetTimer()//重置计时器
    for i:=0;i<b.N;i++{
        fmt.Sprintf("%d",num)
    }
}

这是一个基准测试的例子,从中我们可以看出以下规则:

b.ResetTimer

运行命令 go test -v -run=none -bench=. 查看结果

goos: windows
goarch: amd64
BenchmarkSprintf-8      10000000               134 ns/op
PASS
ok      _/D_/gopath/src/a_tour_of_go/testing    2.797s

-bench :是进行基准测试参数, =. 表示所有的函数,如果要特定函数只需要后面跟函数名,比如 -bench=BenchmarkSprintf

-run=none :的作用是,运行一个none 不存在的单元测试,避免单元测试输出干扰。因为运行基准测试的时候是默认运行我们的单元测试的。我们为了查看方便,就运行一个不存在的单元测试过滤掉。

输出结果表示:

看到函数后面的 -8 了吗?这个表示运行时对应的GOMAXPROCS的值。接着的 10000000 表示运行for循环的次数,也就是调用被测试代码的次数,最后的 134 ns/op 表示每次需要话费134纳秒。

以上是测试时间默认是1秒,也就是1秒的时间,调用1000万次,每次调用花费134纳秒。如果想让测试运行的时间更长,可以通过 -benchtime 指定,比如3秒。

go test -bench=. -benchtime=3s -run=none

goos: windows
goarch: amd64
BenchmarkSprintf-8      30000000               121 ns/op
PASS
ok      _/D_/gopath/src/a_tour_of_go/testing    5.731s

2.2 性能对比

上面那个基准测试的例子,其实是一个int类型转为string类型的例子,标准库里还有几种方法,我们看下哪种性能更加。

func BenchmarkSprintf(b *testing.B){
    num:=10
    b.ResetTimer()
    for i:=0;i<b.N;i++{
        fmt.Sprintf("%d",num)
    }
}

func BenchmarkFormat(b *testing.B){
    num:=int64(10)
    b.ResetTimer()
    for i:=0;i<b.N;i++{
        strconv.FormatInt(num,10)
    }
}

func BenchmarkItoa(b *testing.B){
    num:=10
    b.ResetTimer()
    for i:=0;i<b.N;i++{
        strconv.Itoa(num)
    }
}

运行基准测试,看看结果

运行命令: go test -bench=. -run=none          
goos: windows
goarch: amd64
BenchmarkSprintf-8      10000000               126 ns/op
BenchmarkFormat-8       300000000                3.85 ns/op
BenchmarkItoa-8         300000000                3.93 ns/op
PASS
ok      _/D_/gopath/src/a_tour_of_go/testing    5.893s

从结果上看 strconv.FormatInt 函数是最快的,其次是 strconv.Itoa ,然后是 fmt.Sprintf 最慢。第一个最慢,我们可以通过 -benchmem 找到根本原因。

运行命令: go test -bench=. -benchmem -run=none
goos: windows
goarch: amd64
BenchmarkSprintf-8      10000000               132 ns/op              16 B/op          2 allocs/op
BenchmarkFormat-8       300000000                3.97 ns/op            0 B/op          0 allocs/op
BenchmarkItoa-8         300000000                4.25 ns/op            0 B/op          0 allocs/op
PASS
ok      _/D_/gopath/src/a_tour_of_go/testing    6.073s

-benchmem 可以提供每次操作分配内存的次数,以及每次操作分配的字节数。结果显示,效率高的两个每次操作进行0次内存分配,每次分配操作0个字节,可能是这个太简单了,所以没来得及分配。慢的那个每次操作进行2次内存分配,每次16个字节。 所以效率高低的原因一目了然了。

在代码开发中,对于我们要求性能的地方,编写基准测试非常重要,这有助于我们开发出性能更好的代码。不过性能、可用性、复用性等也要有一个相对的取舍,不能为了追求性能而过度优化。


以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Persuasive Technology

Persuasive Technology

B.J. Fogg / Morgan Kaufmann / 2002-12 / USD 39.95

Can computers change what you think and do? Can they motivate you to stop smoking, persuade you to buy insurance, or convince you to join the Army? "Yes, they can," says Dr. B.J. Fogg, directo......一起来看看 《Persuasive Technology》 这本书的介绍吧!

JSON 在线解析
JSON 在线解析

在线 JSON 格式化工具

在线进制转换器
在线进制转换器

各进制数互转换器

XML 在线格式化
XML 在线格式化

在线 XML 格式化压缩工具