当前位置: 首页 > 工具软件 > Ginkgo > 使用案例 >

Ginkgo学习笔记

邢勇
2023-12-01

By Alex

/ in Go

0 Comments

简介

Ginkgo /ˈɡɪŋkoʊ / 是Go语言的一个行为驱动开发(BDD, Behavior-Driven Development)风格的测试框架,通常和库Gomega一起使用。Ginkgo在一系列的“Specs”中描述期望的程序行为。

Ginkgo集成了Go语言的测试机制,你可以通过 go test来运行Ginkgo测试套件。

Ginkgo

安装

Shell

1

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

起步

创建套件

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

Shell

1

2

cd pkg/books

ginkgo bootstrap

上述命令会生成文件:

pkg/books/books_suite_test.go

Go

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

package books_test

 

import (

    // 使用点号导入,把这两个包导入到当前命名空间

    . "github.com/onsi/ginkgo"

    . "github.com/onsi/gomega"

    "testing"

)

 

func TestBooks(t *testing.T) {

    // 将Ginkgo的Fail函数传递给Gomega,Fail函数用于标记测试失败,这是Ginkgo和Gomega唯一的交互点

    // 如果Gomega断言失败,就会调用Fail进行处理

    RegisterFailHandler(Fail)

 

    // 启动测试套件

    RunSpecs(t, "Books Suite")

}

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

添加Spec

上面的空测试套件没有什么价值,我们需要在此套接下编写测试(Spec)。虽然可以在books_suite_test.go中编写测试,但是推荐分离到独立的文件中,特别是包中有多个需要被测试的源文件的情况下。

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

pkg/books/book_test.go

Go

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

package books_test

 

import (

    . "github.com/onsi/ginkgo"

    . "github.com/onsi/gomega"

    // 为了方便,被测试包被导入当前命名空间

    . "ginkgo-study/pkg/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:

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

33

34

35

36

37

38

39

// 使用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 pages", func() {

            // 通过It来创建一个Spec

            It("should be a novel", func() {

                // Gomega的Expect用于断言

                Expect(longBook.CategoryByLength()).To(Equal("NOVEL"))

            })

        })

 

        Context("With fewer than 300 pages", func() {

            It("should be a short story", func() {

                Expect(shortBook.CategoryByLength()).To(Equal("SHORT STORY"))

            })

        })

    })

})

断言失败

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

Go

1

Fail("Failure reason")

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

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

Go

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包即可:

Go

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:

Go

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("an ErrNoID error is returned", func() {

      })

    })

  })

})

BeforeEach

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

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

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

AfterEach

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

Describe/Context

两者的区别:

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

下面是一个例子:

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

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

// 这是关于Book服务测试

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

    var (

        book Book

        err error

    )

 

    BeforeEach(func() {

        book, err = NewBookFromJSON(`{

            "title":"Les Miserables",

            "author":"Victor Hugo",

            "pages":1488

        }`)

    })

    // 测试加载Book行为

    Describe("loading from JSON", func() {

        // 如果正常解析JSON

        Context("when the JSON parses succesfully", func() {

            It("should populate the fields correctly", func() {

                // 期望                相等

                Expect(book.Title).To(Equal("Les Miserables"))

                Expect(book.Author).To(Equal("Victor Hugo"))

                Expect(book.Pages).To(Equal(1488))

            })

 

            It("should not error", func() {

                // 期望      没有发生错误

                Expect(err).NotTo(HaveOccurred())

            })

        })

        // 如果无法解析JSON

        Context("when the JSON fails to parse", func() {

            BeforeEach(func() {

                // 这是一个BDD反模式,可以用JustBeforeEach

                book, err = NewBookFromJSON(`{

                    "title":"Les Miserables",

                    "author":"Victor Hugo",

                    "pages":1488oops

                }`)

            })

 

            It("should return the zero-value for the book", func() {

                // 期望          为零

                Expect(book).To(BeZero())

            })

 

            It("should error", func() {

                // 期望        发生了错误

                Expect(err).To(HaveOccurred())

            })

        })

    })

 

    Describe("Extracting the author's last name", func() {

        It("should correctly identify and return the last name", func() {

            Expect(book.AuthorLastName()).To(Equal("Hugo"))

        })

    })

})

JustBeforeEach

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

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

33

34

35

36

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

    var (

        book Book

        err error

        json string

    )

    // 准备默认JSON

    BeforeEach(func() {

        json = `{

            "title":"Les Miserables",

            "author":"Victor Hugo",

            "pages":1488

        }`

    })

 

    JustBeforeEach(func() {

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

        book, err = NewBookFromJSON(json)

    })

 

    Describe("loading from JSON", func() {

        Context("when the JSON parses succesfully", func() {

        })

 

        Context("when the JSON fails to parse", func() {

            BeforeEach(func() {

                // 覆盖默认JSON为无效JSON

                json = `{

                    "title":"Les Miserables",

                    "author":"Victor Hugo",

                    "pages":1488oops

                }`

            })

        })

    })

})

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

JustAfterEach

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

BeforeSuite/AfterSuite

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

Go

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

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

Go

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入参:

Go

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 秒

    Ω(runtime.Seconds()).Should(BeNumerically("<", 0.2), "SomethingHard() shouldn't take too long.")

 

    // 录制任意数据

    b.RecordValue("disk usage (in MB)", HowMuchDiskSpaceDidYouUse())

}, 10)

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

CLI

运行测试

Shell

1

2

3

4

5

6

7

# 运行当前目录中的测试

ginkgo

# 运行其它目录中的测试

ginkgo /path/to/package /path/to/other/package ...

 

# 递归运行所有子目录中的测试

ginkgo -r ...

传递参数

传递参数给测试套件:  

Shell

1

ginkgo -- PASS-THROUGHS-ARGS

跳过某些包

Go

1

2

# 跳过某些包

ginkgo -skipPackage=PACKAGES,TO,SKIP

超时控制

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

调试信息

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

其它选项

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

Spec Runner

Pending Spec

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

Go

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:

Go

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提供两种机制满足此需求:

  1.  将容器或Spec标记为Focused,这样默认情况下Ginkgo仅仅运行Focused Spec:

    Go

    1

    2

    3

    FDescribe("some behavior", func() { ... })

    FContext("some scenario", func() { ... })

    FIt("some assertion", func() { ... })

  1. 在命令行中传递正则式: --focus=REGEXP 或/和 --skip=REGEXP,则Ginkgo仅仅运行/跳过匹配的Spec

Parallel Specs

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

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

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

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

Go

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的回调顺序则正好相反:

Go

1

2

3

4

5

6

7

var _ = SynchronizedAfterSuite(func() {

    // 所有节点

    dbClient.Cleanup()

}, func() {

    // 第一个节点

    dbRunner.Stop()

}) 

Gomega

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

联用

和Ginkgo

注册Fail处理器即可:

Go

1

gomega.RegisterFailHandler(ginkgo.Fail)

和Go测试框架

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

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

Go

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

错误处理

对于返回多个值的函数:

Go

1

2

3

4

5

6

func DoSomethingHard() (string, error) {}

 

result, err := DoSomethingHard()

// 断言没有发生错误

Ω(err).ShouldNot(HaveOccurred())

Ω(result).Should(Equal("foo"))

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

Go

1

2

3

func DoSomethingHard() (string, error) {}

 

Ω(DoSomethingSimple()).Should(Succeed())

断言注解

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

Go

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

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

Go

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")) // 检查标准输出

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

Go

1

2

3

Eventually(func() []int {

    return thing.SliceImMonitoring

}, TIMEOUT, POLLING_INTERVAL).Should(HaveLen(2))

Consistently

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

Go

1

2

3

Consistently(func() []int {

    return thing.MemoryUsage()

}, DURATION, POLLING_INTERVAL).Should(BeNumerically("<", 10))

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

Go

1

Consistently(channel).ShouldNot(Receive())

修改默认间隔

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

Go

1

2

3

4

SetDefaultEventuallyTimeout(t time.Duration)

SetDefaultEventuallyPollingInterval(t time.Duration)

SetDefaultConsistentlyDuration(t time.Duration)

SetDefaultConsistentlyPollingInterval(t time.Duration)

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

内置Matcher

相等性 

Go

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

接口相容

Go

1

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

空值/零值

Go

1

2

3

4

5

// 断言ACTUAL为Nil

Ω(ACTUAL).Should(BeNil())

 

// 断言ACTUAL为它的类型的零值,或者是Nil

Ω(ACTUAL).Should(BeZero())

布尔值

Go

1

2

Ω(ACTUAL).Should(BeTrue())

Ω(ACTUAL).Should(BeFalse())

错误

Go

1

2

3

4

5

6

7

8

9

Ω(ACTUAL).Should(HaveOccurred())

 

err := SomethingThatMightFail()

// 没有错误

Ω(err).ShouldNot(HaveOccurred())

 

 

// 如果ACTUAL为Nil则断言成功

Ω(ACTUAL).Should(Succeed())

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

Go

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是不允许的。

通道

Go

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

文件

Go

1

2

3

4

5

6

// 文件或目录存在

Ω(ACTUAL).Should(BeAnExistingFile())

// 断言是普通文件

Ω(ACTUAL).Should(BeARegularFile())

// 断言是目录

BeADirectory

字符串

Go

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

Go

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都属于集合。

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

// 断言为空

Ω(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))

数字/时间

Go

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:

Go

1

Ω(ACTUAL).Should(Panic())

And/Or

Go

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无法满足需要,你可以实现接口:

Go

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。

对所有字段进行断言:

Go

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"),

})

不处理某些字段: 

Go

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"), // 忽略多余字段

})

一个复杂的例子:

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

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(),

}))

 

 类似资料: