目录

ginkgo 测试库

ginkgo

安装

1
go get -u github.com/onsi/ginkgo/ginkgo

快速开始

创建books.go文件

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
package books

import (
	"encoding/json"
	"strings"
)

type Book struct {
	Title string
	Author string
	Pages int
}

func (b Book) AuthorLastName() (string) {
	a := strings.Split(b.Author," ")
	return a[len(a)-1]
}
func (b Book) CategoryByLength() string{
	if b.Pages >300{
		return "NOVEL"
	} else if b.Pages<100 {
		return "SMALL STORY"
	} else {
		return "SHORT STORY"
	}
}
func NewBookFromJSON(js string) (Book,error) {
	var book Book
	err  :=json.Unmarshal([]byte(js),&book)
	return book,err
}

假设我们想给books包编写Ginkgo测试,则首先需要使用命令创建一个Ginkgo test suilte:

1
C:\Users\xx\Desktop\git\ginkgo>ginkgo bootstrap

上述命令会生成文件:

ginkgo_suite_test.go

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
package ginkgo_test

import (
	"testing"
	
	// 使用点号导入,把这两个包导入到当前命名空间
	. "github.com/onsi/ginkgo"
	. "github.com/onsi/gomega"
)

func TestGinkgo(t *testing.T) {
    // 将Ginkgo的Fail函数传递给Gomega,Fail函数用于标记测试失败,这是Ginkgo和Gomega唯一的交互点
    // 如果Gomega断言失败,就会调用Fail进行处理	
	RegisterFailHandler(Fail)
    // 启动测试套件	
	RunSpecs(t, "Ginkgo Suite")
}

现在,使用命令 ginkgo或者 go test即可执行测试套件。

添加Spec

接下来编写测试(Spec)。虽然可以在books_suite_test.go中编写测试,但是推荐分离到独立的文件中,特别是包中有多个需要被测试的源文件的情况下。

执行命令 ginkgo generate book可以为源文件book.go生成测试:

book_test.go

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
package books_test

import (
	. "github.com/onsi/ginkgo"
	. "github.com/onsi/gomega"
	. "books"
)

// 顶级的Describe容器
/*
 Describe块用于组织Specs,其中可以包含任意数量的:
	BeforeEach:在Spec(It块)运行之前执行,嵌套Describe时最外层BeforeEach先执行
	AfterEach:在Spec运行之后执行,嵌套Describe时最内层AfterEach先执行
	JustBeforeEach:在It块,所有BeforeEach之后执行
	Measurement
	可以在Describe块内嵌套Describe、Context、When块
*/

var _ = Describe("Book", func() {

})

我们可以添加一些Specs

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
//使用Describe、Context容器来组织Spec
var _ = Describe("Book", func() {
	var(
		//通过闭包在BeforeEach和It之间共享数据
		longBook Book
		shortBook Book
	)

	//此函数用于初始化Spec的状态,在It块之前运行。如果存在嵌套Describe。则最外面的BeforeEach最先运行
	BeforeEach(func() {
		longBook = Book{
			Title: "Les Miserables",
			Author: "Victor Hugo",
			Pages: 1488,
		}

		shortBook = Book{
			Title: "Fox In Socks",
			Author: "Dr. Seuss",
			Pages: 24,
		}
	})

	Describe("Categorizing book length", func() {
		Context("With more than 300 oages", func() {
			//通过It来创建一个Spec
			It("should be a novel", func() {
				//Gomega的Expect用于断言,断言longBook.CategoryByLength()返回值为NOVEL
				Expect(longBook.CategoryByLength()).To(Equal("NOVEL"))
			})
		})

		Context("With fewer than 300 pages", func() {
			It("shuold be a short story", func() {
				//断言shortBook.CategoryByLength()返回值为SHORT STORY
				Expect(shortBook.CategoryByLength()).To(Equal("SHORT STORY"))
			})
		})
	})
})

使用命令ginkgo 进行测试

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
C:\Users\xx\Desktop\git\ginkgo>ginkgo
...
[Fail] Book Categorizing book length With fewer than 300 pages [It] shuold be a short story 
C:/Users/xx/Desktop/git/ginkgo/book_test.go:54

Ran 2 of 2 Specs in 0.062 seconds
FAIL! -- 1 Passed | 1 Failed | 0 Pending | 0 Skipped

可以看到有1个是通过,有1个失败,失败的是[It] shuold be a short story 的这个,因为按books的逻辑这里调用CategoryByLength小于300的只会返回字符串
SMALL STORY

断言失败

除了调用Gomega之外,你还可以调用Fail函数直接断言失败:

1
Fail("Failure reason")

Fail会记录当前进行的测试,并且触发panic,当前Spec的后续断言不会再进行。

通常情况下Ginkgo会从panic中恢复,并继续下一个测试。但是,如果你启动了一个Goroutine,并在其中触发了断言失败,则不会自动恢复,必须手工调用GinkgoRecover:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
It("panics in a goroutine", func(done Done) {
    go func() {
        // 如果doSomething返回false则下面的defer会确保从panic中恢复
        defer GinkgoRecover()
        // Ω和Expect功能相同
        Ω(doSomething()).Should(BeTrue())
 
        // 在Goroutine中需要关闭done通道
        close(done)
    }()
})

记录日志

全局的GinkgoWriter可以用于写日志。默认情况下GinkgoWriter仅仅在测试失败时将日志Dump到标准输出,以冗长模式( ginkgo -v 或 go test -ginkgo.v)运行Ginkgo时则会立即输出。

如果通过Ctrl + C中断测试,则Ginkgo会立即输出写入到GinkgoWriter的内容。联用 –progress则Ginkgo会在BeforeEach/It/AfterEach之前输出通知到GinkgoWriter,这个特性便于诊断卡住的测试。

传递参数

直接使用flag包即可:

1
2
3
4
var myFlag string
func init() {
    flag.StringVar(&myFlag, "myFlag", "defaultvalue", "myFlag is used to control my behavior")
}

执行测试时使用 ginkgo – –myFlag=xxx传递参数。

测试的结构

It

你可以在Describe、Context这两种容器块内编写Spec,每个Spec写在It块中。

为了贴合自然语言,可以使用It的别名Specify:

1
2
3
4
5
6
7
8
9
Describe("The foobar service", func() {
  Context("when calling Foo()", func() {
    Context("when no ID is provided", func() {
      // 应该返回ErrNoID错误,Specify跟It完全一样
      Specify("an ErrNoID error is returned", func() {
      })
    })
  })
})

BeforeEach

多个Spec共享的、测试准备逻辑,可以放到BeforeEach块中。

在BeforeEach、AfterEach块中进行断言是允许的。

存在容器嵌套时,最外层BeforeEach先运行。

AfterEach

多个Spec共享的、测试清理逻辑,可以放到AfterEach块中。存在容器嵌套时,最内层AfterEach先运行。

Describe/Context

两者的区别:

  1. Describe用于描述你的代码的一个行为
  2. Context用于区分上述行为的不同情况,通常为参数不同导致

例子

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
var _ = Describe("Book", func() {
	var(
		book Book
		err error
		json string
	)

	//此函数用于初始化Spec的状态,在It块之前运行。如果存在嵌套Describe。则最外面的BeforeEach最先运行
	BeforeEach(func() {
		json = `{
			"title":"Les Miserables",
			"author":"Victor Hugo",
			"pages":1488	
		}`
		book ,err = NewBookFromJSON(json)
	})

	//测试加载Book行为
	Describe("loading from JSON", func() {
		//如果正常解析JSON
		Context("解析JSON成功", func() {
			It("是否应该正确填充字段", func() {
				//期望				相等
				Expect(book.Title).To(Equal("Les Miserables"))
				Expect(book.Author).To(Equal("Victor Hugo"))
				Expect(book.Pages).To(Equal(1488))
			})

			It("是否有不应该的错误", func() {
				//期望				没有发生错误
				Expect(err).NotTo(HaveOccurred())
			})
		})

		//如果无法解析JSON
		Context("解析JSON失败", func() {
			BeforeEach(func() {
				// 这是一个BDD反模式,可以用JustBeforeEach
				book ,err = NewBookFromJSON(`{
					"title": "Les Miserables",
					"author":"Victor Hugo",
					"pages":1488oops
				}`)
			})

			It("应该返回nil值", func() {
				//期望				为零
				Expect(book).To(BeZero())
			})

			It("发生错误", func() {
				//期望			发生了错误
				Expect(err).To(HaveOccurred())
				//Expect(err).ToNot(HaveOccurred()) 期望没有发生错误
			})

		})
	})

	Describe("检查作者的名字", func() {
		It("是否正确的识别并返回姓氏", func() {
			Expect(book.AuthorLastName()).To(Equal("Hugo"))
		})
	})
})

JustBeforeEach

上面的例子中,内层Spec需要尝试从无效JSON创建Book,因此它调用NewBookFromJSON对book变量进行覆盖。这种做法是推荐的应该使用JustBeforeEach,这种块在任何BeforeEach执行完毕后执行:

例子

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
package books_test

import (
	. "books"
	. "github.com/onsi/ginkgo"
	. "github.com/onsi/gomega"
)

// 顶级的Describe容器
/*
 Describe块用于组织Specs,其中可以包含任意数量的:
	BeforeEach:在Spec(It块)运行之前执行,嵌套Describe时最外层BeforeEach先执行
	AfterEach:在Spec运行之后执行,嵌套Describe时最内层AfterEach先执行
	JustBeforeEach:在It块,所有BeforeEach之后执行
	Measurement
	可以在Describe块内嵌套Describe、Context、When块
*/

//使用Describe、Context容器来组织Spec
var _ = Describe("Book", func() {
	var(
		book Book
		err error
		json string
	)

	//此函数用于初始化Spec的状态,在It块之前运行。如果存在嵌套Describe。则最外面的BeforeEach最先运行
	BeforeEach(func() {
		json = `{
			"title":"Les Miserables",
			"author":"Victor Hugo",
			"pages":1488	
		}`
	})

	JustBeforeEach(func() {
		// 按需,根据默认数据/无效JSON创建book,避免NewBookFromJSON的重复调用(如果代价很高的话……)
		book, err = NewBookFromJSON(json)
	})

	//测试加载Book行为
	Describe("loading from JSON", func() {
		//如果正常解析JSON
		Context("解析JSON成功", func() {
		})

		//如果无法解析JSON
		Context("解析JSON失败", func() {
			BeforeEach(func() {
				// 覆盖默认JSON为无效JSON
				json = `{
                    "title":"Les Miserables",
                    "author":"Victor Hugo",
                    "pages":1488oops
                }`
			})

			It("应该返回nil值", func() {
				//期望				为零
				Expect(book).To(BeZero())
			})

			It("发生错误", func() {
				//期望			发生了错误
				Expect(err).To(HaveOccurred())
			})

		})
	})

})

在上面的例子中,JustBeforeEach解耦了创建(Creation)和配置(Configuration)这两个阶段。

JustAfterEach

紧跟着It之后运行,在所有AfterEach执行之前。

BeforeSuite/AfterSuite

在整个测试套件执行之前/之后,进行准备/清理。和套件代码写在一起:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
func TestBooks(t *testing.T) {
    RegisterFailHandler(Fail)
 
    RunSpecs(t, "Books Suite")
}
 
var _ = BeforeSuite(func() {
    dbClient = db.NewClient()
    err = dbClient.Connect(dbRunner.Address())
    Expect(err).NotTo(HaveOccurred())
})
 
var _ = AfterSuite(func() {
    dbClient.Cleanup()
})

这两个块都支持异步执行,只需要给函数传递一个Done参数即可。

By

此块用于给逻辑复杂的块添加文档:

1
2
3
4
5
6
7
8
9
var _ = Describe("Browsing the library", func() {
    BeforeEach(func() {
        By("Fetching a token and logging in")
    })
 
    It("should be a pleasant experience", func() {
        By("Entering an aisle")
    })
})

传递给By的字符串会发送给GinkgoWriter,如果测试失败你可以看到。

你可以传递一个可选的函数给By,此函数会立即执行。

性能测试

使用Measure块可以进行性能测试,所有It能够出现的地方,都可以使用Measure。和It一样,Measure会生成一个新的Spec。

传递给Measure的闭包函数必须具有Benchmarker入参:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
Measure("it should do something hard efficiently", func(b Benchmarker) {
    // 执行一段逻辑并即时
    runtime := b.Time("runtime", func() {
        output := SomethingHard()
        Expect(output).To(Equal(17))
    })
 
    // 断言 执行时间             小于 0.2 秒
    Expect(runtime.Seconds()).Should(BeNumerically("<", 0.2), "SomethingHard() shouldn't take too long.")
 
    // 录制任意数据
    b.RecordValue("disk usage (in MB)", HowMuchDiskSpaceDidYouUse())
}, 10)

执行时间、你录制的任意数据的最小、最大、平均值均会在测试完毕后打印出来。

CLI

运行测试

1
2
3
4
5
6
7
# 运行当前目录中的测试
ginkgo
# 运行其它目录中的测试
ginkgo /path/to/package /path/to/other/package ...
 
# 递归运行所有子目录中的测试
ginkgo -r ...

传递参数

传递参数给测试套件:

1
ginkgo -- PASS-THROUGHS-ARGS

跳过某些包

1
2
# 跳过某些包
ginkgo -skipPackage=PACKAGES,TO,SKIP

超时控制

选项 -timeout用于控制套件的最大运行时间,如果超过此时间仍然没有完成,认为测试失败。默认24小时。

调试信息

选项 说明
–reportPassed 打印通过的测试的详细信息
–v 冗长模式
–trace 打印所有错误的调用栈
–progress 打印进度信息

其它选项

选项 说明
-race 启用竞态条件检测
-cover 启用覆盖率测试
-tags 指定编译器标记

Spec Runner

Pending Spec

你可以标记一个Spec或容器为Pending,这样默认情况下不会运行它们。定义块时使用P或X前缀:

1
2
3
4
5
6
7
8
9
PDescribe("some behavior", func() { ... })
PContext("some scenario", func() { ... })
PIt("some assertion")
PMeasure("some measurement")
 
XDescribe("some behavior", func() { ... })
XContext("some scenario", func() { ... })
XIt("some assertion")
XMeasure("some measurement")

默认情况下Ginkgo会为每个Pending的Spec打印描述信息,使用命令行选项 –noisyPendings=false禁止该行为。

SKiping Spec

P或X前缀会在编译期将Spec标记为Pending,你也可以在运行期跳过特定的Spec:

1
2
3
4
5
6
It("should do something, if it can", func() {
    if !someCondition {
        // 跳过此Spec,不需要Return语句
        Skip("special condition wasn't met")
    }
})

Focused Specs

一个很常见的需求是,可以选择运行Spec的一个子集。Ginkgo提供两种机制满足此需求:

  • 将容器或Spec标记为Focused,这样默认情况下Ginkgo仅仅运行Focused Spec:
1
2
3
 FDescribe("some behavior", func() { ... })
 FContext("some scenario", func() { ... })
 FIt("some assertion", func() { ... })
  • 在命令行中传递正则式: –focus=REGEXP 或/和 –skip=REGEXP,则Ginkgo仅仅运行/跳过匹配的Spec

Parallel Specs

Ginkgo支持并行的运行Spec,它实现方式是,创建go test子进程并在其中运行共享队列中的Spec。

使用 ginkgo -p可以启用并行测试,Ginkgo会自动创建适当数量的节点(进程)。你也可以指定节点数量: ginkgo -nodes=N。

如果你的测试代码需要和外部进程交互,或者创建外部进程,在并行测试上下文中需要谨慎的处理。最简单的方式是在BeforeSuite方法中为每个节点创建外部资源。

如果所有Spec需要共享一个外部进程,则可以利用SynchronizedBeforeSuite、SynchronizedAfterSuite:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
var _ = SynchronizedBeforeSuite(func() []byte {
    // 在第一个节点中执行
    port := 4000 + config.GinkgoConfig.ParallelNode
 
    dbRunner = db.NewRunner()
    err := dbRunner.Start(port)
    Expect(err).NotTo(HaveOccurred())
 
    return []byte(dbRunner.Address())
}, func(data []byte) {
    // 在所有节点中执行
    dbAddress := string(data)
 
    dbClient = db.NewClient()
    err = dbClient.Connect(dbAddress)
    Expect(err).NotTo(HaveOccurred())
})

上面的例子,为所有节点创建共享的数据库,然后为每个节点创建独占的客户端。 SynchronizedAfterSuite的回调顺序则正好相反:

1
2
3
4
5
6
7
var _ = SynchronizedAfterSuite(func() {
    // 所有节点
    dbClient.Cleanup()
}, func() {
    // 第一个节点
    dbRunner.Stop()
}) 

Gomega

这是Ginkgo推荐使用的断言(Matcher)库。

联用

和Ginkgo

注册Fail处理器即可:

1
gomega.RegisterFailHandler(ginkgo.Fail)

和Go测试框架

1
2
3
4
5
6
7
8
func TestFarmHasCow(t *testing.T) {
    // 创建Gomega对象
    g := NewGomegaWithT(t)
 
    f := farm.New([]string{"Cow", "Horse"})
    // 进行断言
    g.Expect(f.HasCow()).To(BeTrue(), "Farm should have cow")
}

断言

Ω/Expect

两种断言语法本质是一样的,只是命名风格有些不同:

1
2
3
4
5
6
Ω(ACTUAL).Should(Equal(EXPECTED))
Expect(ACTUAL).To(Equal(EXPECTED))
 
Ω(ACTUAL).ShouldNot(Equal(EXPECTED))
Expect(ACTUAL).NotTo(Equal(EXPECTED))
Expect(ACTUAL).ToNot(Equal(EXPECTED))

错误处理

对于返回多个值的函数:

1
2
3
4
5
6
func DoSomethingHard() (string, error) {}
 
result, err := DoSomethingHard()
// 断言没有发生错误
Ω(err).ShouldNot(HaveOccurred())
Ω(result).Should(Equal("foo"))

对于仅仅返回一个error的函数:

1
2
3
func DoSomethingHard() (string, error) {}
 
Ω(DoSomethingSimple()).Should(Succeed())

断言注解

进行断言时,可以提供格式化字符串,这样断言失败可以方便的知道原因:

1
2
3
4
5
Ω(ACTUAL).Should(Equal(EXPECTED), "My annotation %d", foo)
 
Expect(ACTUAL).To(Equal(EXPECTED), "My annotation %d", foo)
 
Expect(ACTUAL).To(Equal(EXPECTED), func() string { return "My annotation" })

简化输出

断言失败时,Gomega打印牵涉到断言的对象的递归信息,输出可能很冗长。

format包提供了一些全局变量,调整这些变量可以简化输出。

变量 = 默认值 说明
format.MaxDepth = 10 打印对象嵌套属性的最大深度
format.UseStringerRepresentation = false 默认情况下,Gomega不会调用Stringer.String()或GoStringer.GoString()方法来打印对象的字符串表示字符串表示通常人类可读但是信息量较小设置为true则打印字符串表示,可以简化输出
format.PrintContextObjects = false 默认情况下,Gomega不会打印context.Context接口的内容,因为通常非常冗长
format.TruncatedDiff = true 截断长字符串,仅仅打印差异

异步断言

Gomega提供了两个函数,用于异步断言。

传递给Eventually、Consistently的函数,如果返回多个值,则第一个返回值用于匹配,其它值断言为nil或零值。

Eventually

阻塞并轮询参数,直到能通过断言:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
// 参数是闭包,调用函数
Eventually(func() []int {
    return thing.SliceImMonitoring
}).Should(HaveLen(2))
 
// 参数是通道,读取通道
Eventually(channel).Should(BeClosed())
Eventually(channel).Should(Receive())
 
// 参数也可以是普通变量,读取变量
Eventually(myInstance.FetchNameFromNetwork).Should(Equal("archibald"))
 
// 可以和gexec包的Session配合
Eventually(session).Should(gexec.Exit(0)) // 命令最终应当以0退出
Eventually(session.Out).Should(Say("Splines reticulated")) // 检查标准输出

可以指定超时、轮询间隔:

1
2
3
Eventually(func() []int {
    return thing.SliceImMonitoring
}, TIMEOUT, POLLING_INTERVAL).Should(HaveLen(2))

Consistently

检查断言是否在一定时间段内总是通过:

1
2
3
Consistently(func() []int {
    return thing.MemoryUsage()
}, DURATION, POLLING_INTERVAL).Should(BeNumerically("<", 10))

Consistently也可以用来断言最终不会发生的事件,例如下面的例子:

1
Consistently(channel).ShouldNot(Receive())

修改默认间隔

默认情况下,Eventually每10ms轮询一次,持续1s。Consistently每10ms轮询一次,持续100ms。调用下面的函数修改这些默认值:

1
2
3
4
SetDefaultEventuallyTimeout(t time.Duration)
SetDefaultEventuallyPollingInterval(t time.Duration)
SetDefaultConsistentlyDuration(t time.Duration)
SetDefaultConsistentlyPollingInterval(t time.Duration)

这些调用会影响整个测试套件。

内置Matcher

相等性

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// 使用reflect.DeepEqual进行比较
// 如果ACTUAL和EXPECTED都为nil,断言会失败
Ω(ACTUAL).Should(Equal(EXPECTED))
 
// 先把ACTUAL转换为EXPECTED的类型,然后使用reflect.DeepEqual进行比较
// 应当避免用来比较数字
Ω(ACTUAL).Should(BeEquivalentTo(EXPECTED))
 
// 使用 == 进行比较
BeIdenticalTo(expected interface{})

接口相容

1
Ω(ACTUAL).Should(BeAssignableToTypeOf(EXPECTED interface))

空值/零值

1
2
3
4
5
// 断言ACTUAL为Nil
Ω(ACTUAL).Should(BeNil())
 
// 断言ACTUAL为它的类型的零值,或者是Nil
Ω(ACTUAL).Should(BeZero())

布尔值

1
2
Ω(ACTUAL).Should(BeTrue())
Ω(ACTUAL).Should(BeFalse())

错误

1
2
3
4
5
6
7
8
9
Ω(ACTUAL).Should(HaveOccurred())
 
err := SomethingThatMightFail()
// 没有错误
Ω(err).ShouldNot(HaveOccurred())
 
 
// 如果ACTUAL为Nil则断言成功
Ω(ACTUAL).Should(Succeed())

可以对错误进行细粒度的匹配:

1
Ω(ACTUAL).Should(MatchError(EXPECTED))

上面的EXPECTED可以是:

  1. 字符串:则断言ACTUAL.Error()与之相等
  2. Matcher:则断言ACTUAL.Error()与之进行匹配
  3. error:则ACTUAL和error基于reflect.DeepEqual()进行比较
  4. 实现了error接口的非Nil指针,调用 errors.As(ACTUAL, EXPECTED)进行检查

不符合以上条件的EXPECTED是不允许的。

通道

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
// 断言通道是否关闭
// Gomega会尝试读取通道进行判断,因此你需要注意:
//    如果是缓冲通道,你需要先将通道读干净
//    如果你后续需要再次读取通道,注意此断言的影响
Ω(ACTUAL).Should(BeClosed())
Ω(ACTUAL).ShouldNot(BeClosed())
 
 
// 断言能够从通道里面读取到消息
// 此断言会立即返回,如果通道已经关闭,则下面的断言失败
Ω(ACTUAL).Should(Receive(<optionalPointer>))
 
 
 
// 断言能够无阻塞的发送消息
Ω(ACTUAL).Should(BeSent(VALUE))

文件

1
2
3
4
5
6
// 文件或目录存在
Ω(ACTUAL).Should(BeAnExistingFile())
// 断言是普通文件
Ω(ACTUAL).Should(BeARegularFile())
// 断言是目录
BeADirectory

字符串

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// 子串判断                        fmt.Sprintf(STRING, ARGS...)
Ω(ACTUAL).Should(ContainSubstring(STRING, ARGS...))
 
// 前缀判断
Ω(ACTUAL).Should(HavePrefix(STRING, ARGS...))
 
// 后缀判断
Ω(ACTUAL).Should(HaveSuffix(STRING, ARGS...))
 
 
// 正则式匹配
Ω(ACTUAL).Should(MatchRegexp(STRING, ARGS...))

JSON/XML/YML

1
2
3
Ω(ACTUAL).Should(MatchJSON(EXPECTED))
Ω(ACTUAL).Should(MatchXML(EXPECTED))
Ω(ACTUAL).Should(MatchYAML(EXPECTED))

ACTUAL、EXPECTED可以是string、[]byte、Stringer。如果两者转换为对象是reflect.DeepEqual的则匹配。

集合

string, array, map, chan, slice都属于集合。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 断言为空
Ω(ACTUAL).Should(BeEmpty())
 
// 断言长度
Ω(ACTUAL).Should(HaveLen(INT))
 
// 断言容量
Ω(ACTUAL).Should(HaveCap(INT))
 
// 断言包含元素
Ω(ACTUAL).Should(ContainElement(ELEMENT))
 
// 断言等于                   其中之一
Ω(ACTUAL).Should(BeElementOf(ELEMENT1, ELEMENT2, ELEMENT3, ...))
 
 
// 断言元素相同,不考虑顺序
Ω(ACTUAL).Should(ConsistOf(ELEMENT1, ELEMENT2, ELEMENT3, ...))
Ω(ACTUAL).Should(ConsistOf([]SOME_TYPE{ELEMENT1, ELEMENT2, ELEMENT3, ...}))
 
// 断言存在指定的键,仅用于map
Ω(ACTUAL).Should(HaveKey(KEY))
// 断言存在指定的键值对,仅用于map
Ω(ACTUAL).Should(HaveKeyWithValue(KEY, VALUE))

数字/时间

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// 断言数字意义(类型不感知)上的相等
Ω(ACTUAL).Should(BeNumerically("==", EXPECTED))
 
// 断言相似,无差不超过THRESHOLD(默认1e-8)
Ω(ACTUAL).Should(BeNumerically("~", EXPECTED, <THRESHOLD>))
 
 
Ω(ACTUAL).Should(BeNumerically(">", EXPECTED))
Ω(ACTUAL).Should(BeNumerically(">=", EXPECTED))
Ω(ACTUAL).Should(BeNumerically("<", EXPECTED))
Ω(ACTUAL).Should(BeNumerically("<=", EXPECTED))
 
Ω(number).Should(BeBetween(0, 10))

比较时间时使用BeTemporally函数,和BeNumerically类似。

Panic

断言会发生Panic:

1
Ω(ACTUAL).Should(Panic())

And/Or

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
Expect(number).To(SatisfyAll(
            BeNumerically(">", 0),
            BeNumerically("<", 10)))
// 或者
Expect(msg).To(And(
            Equal("Success"),
            MatchRegexp(`^Error .+$`)))
 
 
 
Ω(ACTUAL).Should(SatisfyAny(MATCHER1, MATCHER2, ...))
// 或者
Ω(ACTUAL).Should(Or(MATCHER1, MATCHER2, ...))

自定义Matcher

如果内置Matcher无法满足需要,你可以实现接口:

1
2
3
4
5
type GomegaMatcher interface {
    Match(actual interface{}) (success bool, err error)
    FailureMessage(actual interface{}) (message string)
    NegatedFailureMessage(actual interface{}) (message string)
}

辅助工具

ghttp

用于测试HTTP客户端,此包提供了Mock HTTP服务器的能力。

gbytes

gbytes.Buffer实现了接口io.WriteCloser,能够捕获到内存缓冲的输入。配合使用 gbytes.Say能够对流数据进行有序的断言。

gexec

简化了外部进程的测试,可以:

  1. 编译Go二进制文件
  2. 启动外部进程
  3. 发送信号并等待外部进程退出
  4. 基于退出码进行断言
  5. 将输出流导入到gbytes.Buffer进行断言

gstruct

此包用于测试复杂的Go结构,提供了结构、切片、映射、指针相关的Matcher。

对所有字段进行断言:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
actual := struct{
    A int
    B bool
    C string
}{5, true, "foo"}
Expect(actual).To(MatchAllFields(Fields{
    "A": BeNumerically("<", 10),
    "B": BeTrue(),
    "C": Equal("foo"),
})

不处理某些字段:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
Expect(actual).To(MatchFields(IgnoreExtras, Fields{
    "A": BeNumerically("<", 10),
    "B": BeTrue(),
    // 忽略C字段
})
 
 
Expect(actual).To(MatchFields(IgnoreMissing, Fields{
    "A": BeNumerically("<", 10),
    "B": BeTrue(),
    "C": Equal("foo"),
    "D": Equal("bar"), // 忽略多余字段
})

一个复杂的例子:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
coreID := func(element interface{}) string {
    return strconv.Itoa(element.(CoreStats).Index)
}
Expect(actual).To(MatchAllFields(Fields{
    // 忽略此字段
    "Name":      Ignore(),
    // 时间断言
    "StartTime": BeTemporally(">=", time.Now().Add(-100 * time.Hour)),
    //     解引用后再断言
    "CPU": PointTo(MatchAllFields(Fields{
        "Time":                 BeTemporally(">=", time.Now().Add(-time.Hour)),
        "UsageNanoCores":       BeNumerically("~", 1E9, 1E8),
        "UsageCoreNanoSeconds": BeNumerically(">", 1E6),
        //       包含匹配的元素, 抽取ID的函数
        "Cores": MatchElements(coreID, IgnoreExtras, Elements{
            // ID: Matcher
            "0": MatchAllFields(Fields{
                Index: Ignore(),
                "UsageNanoCores":       BeNumerically("<", 1E9),
                "UsageCoreNanoSeconds": BeNumerically(">", 1E5),
            }),
            "1": MatchAllFields(Fields{
                Index: Ignore(),
                "UsageNanoCores":       BeNumerically("<", 1E9),
                "UsageCoreNanoSeconds": BeNumerically(">", 1E5),
            }),
        }),
    }))
    "Logs":               m.Ignore(),
}))