Go语言单元测试基础从入门到放弃_第1页
Go语言单元测试基础从入门到放弃_第2页
Go语言单元测试基础从入门到放弃_第3页
Go语言单元测试基础从入门到放弃_第4页
Go语言单元测试基础从入门到放弃_第5页
已阅读5页,还剩12页未读 继续免费阅读

下载本文档

版权说明:本文档由用户提供并上传,收益归属内容提供方,若内容存在侵权,请进行举报或认领

文档简介

第Go语言单元测试基础从入门到放弃目录Go语言测试gotest工具单元测试函数格式单元测试示例gotest-vgotest-run回归测试跳过某些测试用例子测试表格驱动测试介绍示例并行测试使用工具生成测试代码测试覆盖率testify/assert安装使用示例总结

Go语言测试

这是Go单测从入门到放弃系列教程的第0篇,主要讲解在Go语言中如何做单元测试以及介绍了表格驱动测试、回归测试,并且介绍了常用的断言工具。

gotest工具

Go语言中的测试依赖gotest命令。编写测试代码和编写普通的Go代码过程是类似的,并不需要学习新的语法、规则或工具。

gotest命令是一个按照一定约定和组织的测试代码的驱动程序。在包目录内,所有以_test.go为后缀名的源代码文件都是gotest测试的一部分,不会被gobuild编译到最终的可执行文件中。

在*_test.go文件中有三种类型的函数,单元测试函数、基准测试函数和示例函数。

类型格式作用测试函数函数名前缀为Test测试程序的一些逻辑行为是否正确基准函数函数名前缀为Benchmark测试函数的性能示例函数函数名前缀为Example为文档提供示例文档

gotest命令会遍历所有的*_test.go文件中符合上述命名规则的函数,然后生成一个临时的main包用于调用相应的测试函数,然后构建并运行、报告测试结果,最后清理测试中生成的临时文件。

单元测试函数

格式

每个测试函数必须导入testing包,测试函数的基本格式(签名)如下:

func

TestName(t

*testing.T){

//

...

测试函数的名字必须以Test开头,可选的后缀名必须以大写字母开头,举几个例子:

func

TestAdd(t

*testing.T){

...

}

func

TestSum(t

*testing.T){

...

}

func

TestLog(t

*testing.T){

...

}

其中参数t用于报告测试失败和附加的日志信息。testing.T的拥有的方法如下:

func

(c

*T)

Cleanup(func())

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

(c

*T)

Skip(args

...interface{})

func

(c

*T)

SkipNow()

func

(c

*T)

Skipf(format

string,

args

...interface{})

func

(c

*T)

Skipped()

bool

func

(c

*T)

TempDir()

string

单元测试示例

就像细胞是构成我们身体的基本单位,一个软件程序也是由很多单元组件构成的。单元组件可以是函数、结构体、方法和最终用户可能依赖的任意东西。总之我们需要确保这些组件是能够正常运行的。单元测试是一些利用各种方法测试单元组件的程序,它会将结果与预期输出进行比较。

接下来,我们在base_demo包中定义了一个Split函数,具体实现如下:

//

base_demo/split.go

package

base_demo

import

"strings"

//

Split

把字符串s按照给定的分隔符sep进行分割返回字符串切片

func

Split(s,

sep

string)

(result

[]string)

{

i

:=

strings.Index(s,

sep)

for

i

-1

{

result

=

append(result,

s[:i])

s

=

s[i+1:]

i

=

strings.Index(s,

sep)

result

=

append(result,

s)

return

在当前目录下,我们创建一个split_test.go的测试文件,并定义一个测试函数如下:

//

split/split_test.go

package

split

import

(

"reflect"

"testing"

func

TestSplit(t

*testing.T)

{

//

测试函数名必须以Test开头,必须接收一个*testing.T类型参数

got

:=

Split("a:b:c",

":")

//

程序输出的结果

want

:=

[]string{"a",

"b",

"c"}

//

期望的结果

if

!reflect.DeepEqual(want,

got)

{

//

因为slice不能比较直接,借助反射包中的方法比较

t.Errorf("expected:%v,

got:%v",

want,

got)

//

测试失败输出错误提示

此时split这个包中的文件如下:

ls

-l

total

16

-rw-r--r--

1

liwenzhou

staff

408

4

29

15:50

split.go

-rw-r--r--

1

liwenzhou

staff

466

4

29

16:04

split_test.go

在当前路径下执行gotest命令,可以看到输出结果如下:

❯gotest

PASS

okgolang-unit-test-demo/base_demo0.005s

gotest-v

一个测试用例有点单薄,我们再编写一个测试使用多个字符切割字符串的例子,在split_test.go中添加如下测试函数:

func

TestSplitWithComplexSep(t

*testing.T)

{

got

:=

Split("abcd",

"bc")

want

:=

[]string{"a",

"d"}

if

!reflect.DeepEqual(want,

got)

{

t.Errorf("expected:%v,

got:%v",

want,

got)

现在我们有多个测试用例了,为了能更好的在输出结果中看到每个测试用例的执行情况,我们可以为gotest命令添加-v参数,让它输出完整的测试结果。

❯gotest-v

===RUNTestSplit

---PASS:TestSplit(0.00s)

===RUNTestSplitWithComplexSep

split_test.go:20:expected:[ad],got:[acd]

---FAIL:TestSplitWithComplexSep(0.00s)

FAIL

exitstatus1

FAILgolang-unit-test-demo/base_demo0.009s

从上面的输出结果我们能清楚的看到是TestSplitWithComplexSep这个测试用例没有测试通过。

gotest-run

单元测试的结果表明split函数的实现并不可靠,没有考虑到传入的sep参数是多个字符的情况,下面我们来修复下这个Bug:

package

base_demo

import

"strings"

//

Split

把字符串s按照给定的分隔符sep进行分割返回字符串切片

func

Split(s,

sep

string)

(result

[]string)

{

i

:=

strings.Index(s,

sep)

for

i

-1

{

result

=

append(result,

s[:i])

s

=

s[i+len(sep):]

//

这里使用len(sep)获取sep的长度

i

=

strings.Index(s,

sep)

result

=

append(result,

s)

return

在执行gotest命令的时候可以添加-run参数,它对应一个正则表达式,只有函数名匹配上的测试函数才会被gotest命令执行。

例如通过给gotest添加-run=Sep参数来告诉它本次测试只运行TestSplitWithComplexSep这个测试用例:

go

test

-run=Sep

-v

===

RUN

TestSplitWithComplexSep

---

PASS:

TestSplitWithComplexSep

(0.00s)

ok

golang-unit-test-demo/base_demo

0.010s

最终的测试结果表情我们成功修复了之前的Bug。

回归测试

我们修改了代码之后仅仅执行那些失败的测试用例或新引入的测试用例是错误且危险的,正确的做法应该是完整运行所有的测试用例,保证不会因为修改代码而引入新的问题。

go

test

-v

===

RUN

TestSplit

---

PASS:

TestSplit

(0.00s)

===

RUN

TestSplitWithComplexSep

---

PASS:

TestSplitWithComplexSep

(0.00s)

ok

golang-unit-test-demo/base_demo

0.011s

测试结果表明我们的单元测试全部通过。

通过这个示例我们可以看到,有了单元测试就能够在代码改动后快速进行回归测试,极大地提高开发效率并保证代码的质量。

跳过某些测试用例

为了节省时间支持在单元测试时跳过某些耗时的测试用例。

func

TestTimeConsuming(t

*testing.T)

{

if

testing.Short()

{

t.Skip("short模式下会跳过该测试用例")

}

...

当执行gotest-short时就不会执行上面的TestTimeConsuming测试用例。

子测试

在上面的示例中我们为每一个测试数据编写了一个测试函数,而通常单元测试中需要多组测试数据保证测试的效果。Go1.7+中新增了子测试,支持在测试函数中使用t.Run执行一组测试用例,这样就不需要为不同的测试数据定义多个测试函数了。

func

TestXXX(t

*testing.T){

t.Run("case1",

func(t

*testing.T){...})

t.Run("case2",

func(t

*testing.T){...})

t.Run("case3",

func(t

*testing.T){...})

表格驱动测试

介绍

编写好的测试并非易事,但在许多情况下,表格驱动测试可以涵盖很多方面:表格里的每一个条目都是一个完整的测试用例,包含输入和预期结果,有时还包含测试名称等附加信息,以使测试输出易于阅读。

使用表格驱动测试能够很方便的维护多个测试用例,避免在编写单元测试时频繁的复制粘贴。

表格驱动测试的步骤通常是定义一个测试用例表格,然后遍历表格,并使用t.Run对每个条目执行必要的测试。

表格驱动测试不是工具、包或其他任何东西,它只是编写更清晰测试的一种方式和视角。

示例

官方标准库中有很多表格驱动测试的示例,例如fmt包中的测试代码:

var

flagtests

=

[]struct

{

in

string

out

string

{"%a",

"[%a]"},

{"%-a",

"[%-a]"},

{"%+a",

"[%+a]"},

{"%#a",

"[%#a]"},

{"%

a",

"[%

a]"},

{"%0a",

"[%0a]"},

{"%1.2a",

"[%1.2a]"},

{"%-1.2a",

"[%-1.2a]"},

{"%+1.2a",

"[%+1.2a]"},

{"%-+1.2a",

"[%+-1.2a]"},

{"%-+1.2abc",

"[%+-1.2a]bc"},

{"%-1.2abc",

"[%-1.2a]bc"},

func

TestFlagParser(t

*testing.T)

{

var

flagprinter

flagPrinter

for

_,

tt

:=

range

flagtests

{

t.Run(tt.in,

func(t

*testing.T)

{

s

:=

Sprintf(tt.in,

amp;flagprinter)

if

s

!=

tt.out

{

t.Errorf("got

%q,

want

%q",

s,

tt.out)

通常表格是匿名结构体数组切片,可以定义结构体或使用已经存在的结构进行结构体数组声明。name属性用来描述特定的测试用例。

接下来让我们试着自己编写表格驱动测试:

func

TestSplitAll(t

*testing.T)

{

//

定义测试表格

//

这里使用匿名结构体定义了若干个测试用例

//

并且为每个测试用例设置了一个名称

tests

:=

[]struct

{

name

string

input

string

sep

string

want

[]string

{"base

case",

"a:b:c",

":",

[]string{"a",

"b",

"c"}},

{"wrong

sep",

"a:b:c",

",",

[]string{"a:b:c"}},

{"more

sep",

"abcd",

"bc",

[]string{"a",

"d"}},

{"leading

sep",

"沙河有沙又有河",

"沙",

[]string{"",

"河有",

"又有河"}},

//

遍历测试用例

for

_,

tt

:=

range

tests

{

t.Run(,

func(t

*testing.T)

{

//

使用t.Run()执行子测试

got

:=

Split(tt.input,

tt.sep)

if

!reflect.DeepEqual(got,

tt.want)

{

t.Errorf("expected:%#v,

got:%#v",

tt.want,

got)

在终端执行gotest-v,会得到如下测试输出结果:

❯gotest-v

===RUNTestSplit

---PASS:TestSplit(0.00s)

===RUNTestSplitWithComplexSep

---PASS:TestSplitWithComplexSep(0.00s)

===RUNTestSplitAll

===RUNTestSplitAll/base_case

===RUNTestSplitAll/wrong_sep

===RUNTestSplitAll/more_sep

===RUNTestSplitAll/leading_sep

---PASS:TestSplitAll(0.00s)

---PASS:TestSplitAll/base_case(0.00s)

---PASS:TestSplitAll/wrong_sep(0.00s)

---PASS:TestSplitAll/more_sep(0.00s)

---PASS:TestSplitAll/leading_sep(0.00s)

PASS

okgolang-unit-test-demo/base_demo0.010s

并行测试

表格驱动测试中通常会定义比较多的测试case,在Go语言中很容易发挥自身并发优势将表格驱动测试并行化,可以查看下面的代码示例。

func

TestSplitAll(t

*testing.T)

{

t.Parallel()

//

TLog

标记为能够与其他测试并行运行

//

定义测试表格

//

这里使用匿名结构体定义了若干个测试用例

//

并且为每个测试用例设置了一个名称

tests

:=

[]struct

{

name

string

input

string

sep

string

want

[]string

{"base

case",

"a:b:c",

":",

[]string{"a",

"b",

"c"}},

{"wrong

sep",

"a:b:c",

",",

[]string{"a:b:c"}},

{"more

sep",

"abcd",

"bc",

[]string{"a",

"d"}},

{"leading

sep",

"沙河有沙又有河",

"沙",

[]string{"",

"河有",

"又有河"}},

//

遍历测试用例

for

_,

tt

:=

range

tests

{

tt

:=

tt

//

注意这里重新声明tt变量(避免多个goroutine中使用了相同的变量)

t.Run(,

func(t

*testing.T)

{

//

使用t.Run()执行子测试

t.Parallel()

//

将每个测试用例标记为能够彼此并行运行

got

:=

Split(tt.input,

tt.sep)

if

!reflect.DeepEqual(got,

tt.want)

{

t.Errorf("expected:%#v,

got:%#v",

tt.want,

got)

使用工具生成测试代码

社区里有很多自动生成表格驱动测试函数的工具,比如gotests等,很多编辑器如Goland也支持快速生成测试文件。这里简单演示一下gotests的使用。

安装

go

get

-u

/cweill/gotests/...

执行

gotests

-all

-w

split.go

上面的命令表示,为split.go文件的所有函数生成测试代码至split_test.go文件(目录下如果事先存在这个文件就不再生成)。

生成的测试代码大致如下:

package

base_demo

import

(

"reflect"

"testing"

func

TestSplit(t

*testing.T)

{

type

args

struct

{

s

string

sep

string

tests

:=

[]struct

{

name

string

args

args

wantResult

[]string

//

TODO:

Add

test

cases.

for

_,

tt

:=

range

tests

{

t.Run(,

func(t

*testing.T)

{

if

gotResult

:=

Split(tt.args.s,

tt.args.sep);

!reflect.DeepEqual(gotResult,

tt.wantResult)

{

t.Errorf("Split()

=

%v,

want

%v",

gotResult,

tt.wantResult)

代码格式与我们上面的类似,只需要在TODO位置添加我们的测试逻辑就可以了。

测试覆盖率

测试覆盖率是指代码被测试套件覆盖的百分比。通常我们使用的都是语句的覆盖率,也就是在测试中至少被运行一次的代码占总代码的比例。在公司内部一般会要求测试覆盖率达到80%左右。

Go提供内置功能来检查你的代码覆盖率。我们可以使用gotest-cover来查看测试覆盖率。例如:

go

test

-cover

coverage:

100.0%

of

statements

ok

golang-unit-test-demo/base_demo

0.009s

从上面的结果可以看到我们的测试用例覆盖了100%的代码。

Go还提供了一个额外的-coverprofile参数,用来将覆盖率相关的记录信息输出到一个文件。例如:

go

test

-cover

-coverprofile=c.out

coverage:

100.0%

of

statements

ok

golang-unit-test-demo/base_demo

0.009s

上面的命令会将覆盖率相关的信息输出到当前文件夹下面的c.out文件中。

tree

.

├──

c.out

├──

split.go

└──

split_test.go

然后我们执行gotoolcover-html=c.out,使用cover工具来处理生成的记录信息,该命令会打开本地的浏览器窗口生成一个HTML报告。

上图中每个用绿色标记的语句块表示被覆盖了,而红色的表示没有被覆盖。

testify/assert

testify是一个社区非常流行的Go单元测试工具包,其中使用最多的功能就是它提供的断言工具testify/assert或testify/require。

安装

go

get

/stretchr/testify

使用示例

我们在写单元测试的时候,通常需要使用断言来校验测试结果,但是由于Go语言中没有提供断言,所以我们会写出很多的if...else...语句。而testify/assert为我们提供了很多常用的断言函数,并且能够输出友好、易于阅读的错误描述信息。

比如我们之前在TestSplit测试函数中就使用了reflect.DeepEqual来判断期望结果与实际结果是否一致。

t.Run(,

func(t

*testing.T)

{

//

使用t.Run()执行子测试

got

:=

Split(tt.input,

tt.sep)

if

!reflect.DeepEqual(got,

tt.want)

{

t.Errorf("expected:%#v,

got:%#v",

tt.want,

got)

使用testify/assert之后就能将上述判断过程简化如下:

t.Run(,

func(t

*testing.T)

{

//

使用t.Ru

温馨提示

  • 1. 本站所有资源如无特殊说明,都需要本地电脑安装OFFICE2007和PDF阅读器。图纸软件为CAD,CAXA,PROE,UG,SolidWorks等.压缩文件请下载最新的WinRAR软件解压。
  • 2. 本站的文档不包含任何第三方提供的附件图纸等,如果需要附件,请联系上传者。文件的所有权益归上传用户所有。
  • 3. 本站RAR压缩包中若带图纸,网页内容里面会有图纸预览,若没有图纸预览就没有图纸。
  • 4. 未经权益所有人同意不得将文件中的内容挪作商业或盈利用途。
  • 5. 人人文库网仅提供信息存储空间,仅对用户上传内容的表现方式做保护处理,对用户上传分享的文档内容本身不做任何修改或编辑,并不能对任何下载内容负责。
  • 6. 下载文件中如有侵权或不适当内容,请与我们联系,我们立即纠正。
  • 7. 本站不保证下载资源的准确性、安全性和完整性, 同时也不承担用户因使用这些下载资源对自己和他人造成任何形式的伤害或损失。

评论

0/150

提交评论