Ginkgo&Gomega

前言

最近公司项目实现标准化,打算给项目加上单元测试和集成测试,在Go的项目中由调研后打算采用GinkgoGomega,看了下官方文档和github上的测试后写下点心得

Ginkgo是Go语言的一个行为驱动开发(BDD)风格的测试框架,通常和Gomega一起使用

Gomega是一个匹配/断言库,通常与Ginkgo测试框架搭配使用

Ginkgo官方中文文档

Gomega官方文档

Ginkgo使用笔记

下面的文章对上面的文档进行了参考和引用

对于测试的方法和使用请参考具体的文档,本文章主要介绍实战的使用

安装

安装Ginkgo

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

安装Gomega

1
go get github.com/onsi/gomega/...

开始

Ginkgo与Go自带测试挂钩,允许使用go test运行Ginkgo套件

创建套件

如果要为一个包编写Ginkgo测试,首先需要使用命令创建套件

1
2
cd pkg/book #假设要为book包编写测试
ginkgo bootstrap

上诉命令会生成文件:

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中编写测试,但是推荐分离到独立的文件中,特别是包中有多个需要被测试的源文件的情况下

1
ginkgo generate book #执行该命令可以生成源文件为book.go的测试
1
2
3
var _ = Describe("Book", func() {

})
  • Describe块用来组织Specs,其中可以包含任意数量的
  • It:可以在DescribeContext这两种容器块内编写Spec,每个Spec写在It块中;为了贴合自然语言,可以使用It的别名Specify
  • 使用DescribeContext来标识组织代码的具体行为
  • BeforeEach:在Spec(It块)运行之前执行,嵌套Describe时最外层BeforeEach先执行
  • AfterEach:在Spec运行之后执行,嵌套Describe时最内层AfterEach先执行
  • JustBeforeEach:在It块,所有BeforeEach之后执行,在It之前运行

实战

现在要对网页后端的多个API接口进行集成测试

初步

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
73
74
75
76
77
78
79
80
81
var _ = Describe("Endpoint", func() {
var (
reqJSON string
res *http.Response
req *http.Request
client *http.Client
url string
method string
err error
)

BeforeEach(func() {
client = &http.Client{}
res = &http.Response{}
req = &http.Request{}
})

Context("test test1", func() {
BeforeEach(func() {
url = "http://127.0.0.1:8080/test1"
method = "GET"
req, err = http.NewRequest(method, url, strings.NewReader(reqJSON))
gomega.Expect(err).ShouldNot(gomega.HaveOccurred())

req.Header.Add("xxx", "xxx")
req.Header.Add("xxx", "xxx")

res, err = client.Do(req)
gomega.Expect(err).ShouldNot(gomega.HaveOccurred())
})
//直接对比res的status_code,简便
It("test1 success", func() {
gomega.Expect(res.StatusCode).Should(gomega.Equal(http.StatusOK))
})

})

Context("test test2", func() {
BeforeEach(func() {
url = "http://127.0.0.1:8080/test2/1"
method = "GET"
req, err = http.NewRequest(method, url, strings.NewReader(reqJSON))
gomega.Expect(err).ShouldNot(gomega.HaveOccurred())

req.Header.Add("xxx", "xxx")

res, err = client.Do(req)
gomega.Expect(err).ShouldNot(gomega.HaveOccurred())
})
//实现接口后可以自定义匹配规则,匹配方式灵活,并且可以打印自定义错误消息
It("test2 success", func() {
gomega.Expect(res).To(test.RepresentJSONObject(http.StatusOK))
})

})

Context("test test3", func() {
BeforeEach(func() {
url = "http://127.0.0.1:8080/test3/1"
method = "PUT"
reqJSON = "{\"id\":1}"
req, err = http.NewRequest(method, url, strings.NewReader(reqJSON))
gomega.Expect(err).ShouldNot(gomega.HaveOccurred())

req.Header.Add("xxx", "xxx")
req.Header.Add("Content-Type", "application/json")

res, err = client.Do(req)
gomega.Expect(err).ShouldNot(gomega.HaveOccurred())
})
//通过自定义匹配规则返回的err判定
It("test3 success", func() {
_, err := test.RepresentJSONObject(http.StatusOK).Match(res)
gomega.Expect(err).ShouldNot(gomega.HaveOccurred())
})
//通过自定义匹配规则返回的true判定,只答应ture和false的消息
It("test3 no err", func() {
gomega.Expect(test.RepresentJSONObject(http.StatusOK).Match(res)).Should(gomega.Equal(true))
})
})
})
  • 上诉的思想就是每个测试组件都提前定义好requestresponse,通过发送给clientrequest加上Gomega断言收到的response来进行测试
  • 把每个测试组件都要使用的变脸,如url、method、client等放最初的全局变量
  • 每次使用前都要重新初始化clientrequestresponse则放入BeforeEach(如果使用了response.Body)可以使用AfterEach进行关闭

对上面不同的Gomega断言类型进行说明

  • gomega.Expect(res.StatusCode).Should(gomega.Equal(http.StatusOK))

    ShouldTo相同,表示肯定,gomega.Equal则是表示断言相等,连起来就是我们实际得到的res.StatusCode应该和http.StatusOK相同,相同测试成功,不同测试失败

  • gomega.Expect(err).ShouldNot(gomega.HaveOccurred())

    ShouldNotNotTo相同,表示否定,gomega.HaveOccurred()指示non-nillerror则成功,连起来就是如果出现non-nilerror就测试失败,errornil则测试成功,则是非常典型的Go错误测试模式

  • gomega.Expect(res).To(test.RepresentJSONObject(http.StatusOK))

    自定义匹配规则,只要实现MatchFailureMessageNegatedFailureMessage方法的接口就行,好处就是匹配规则灵活多变(传入的http.StatusOK作为expected去Match Expect中的 res),匹配结果返回的是falsenon-err则为测试失败

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
func RepresentJSONObject(expected interface{}) types.GomegaMatcher {
return &representJSONMatcher{
expected: expected,
}
}

type representJSONMatcher struct {
expected interface{}
}

func (matcher *representJSONMatcher) Match(actual interface{}) (success bool, err error) {
response, ok := actual.(*http.Response)
if !ok {
return false, fmt.Errorf("RepresentJSONObject matcher expects an http.Response")
}

if !reflect.DeepEqual(matcher.expected.(int), response.StatusCode) {
return false, fmt.Errorf("actual and expect no equal ,expect is %d,actual is %d", matcher.expected.(int), response.StatusCode)
}
return true, nil
}

func (matcher *representJSONMatcher) FailureMessage(actual interface{}) (message string) {
return fmt.Sprintf("Expected\n\t%#v\nto contain the JSON representation of\n\t%#v", actual, matcher.expected)
}

func (matcher *representJSONMatcher) NegatedFailureMessage(actual interface{}) (message string) {
return fmt.Sprintf("Expected\n\t%#v\nnot to contain the JSON representation of\n\t%#v", actual, matcher.expected)
}

改进

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
var _ = Describe("Endpoint", func() {

JustAfterEach(func() {
warpper.InitTCli()
})

Context("test test1", func() {
BeforeEach(func() {
err := warpper.TCli.Do("GET", "/test1", "")
gomega.Expect(err).ShouldNot(gomega.HaveOccurred())
})

It("test1 success", func() {
gomega.Expect(warpper.TCli.Res).To(warpper.RepresentJSONObject(http.StatusOK))
})

})

Context("test test2", func() {
BeforeEach(func() {
err := warpper.TCli.Do("GET", "/test/1", "")
gomega.Expect(err).ShouldNot(gomega.HaveOccurred())
})

It("test2 success", func() {
gomega.Expect(warpper.TCli.Res).To(warpper.RepresentJSONObject(http.StatusOK))
})

})

Context("update endpoint", func() {
BeforeEach(func() {
reqJSON := "{\"id\":1}"
err := warpper.TCli.Do("PUT", "/test3/1", reqJSON)
gomega.Expect(err).ShouldNot(gomega.HaveOccurred())
})
It("update endpoint success", func() {
gomega.Expect(warpper.TCli.Res).To(warpper.RepresentJSONObject(http.StatusOK))
})
})
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
type TeClient struct {
Res *http.Response
Req *http.Request
Client *http.Client
Err error
}

var TCli *TeClient

func InitTCli() {
TCli = &TeClient{
Res: &http.Response{},
Req: &http.Request{},
Client: &http.Client{},
Err: nil,
}
}
  • 采用自定义封装匹配规则和gomega.HaveOccurred()错误断言

  • 对变量定义和初始化进行封装,因为每次在调用后都要重新初始化TCli,所以采用创建套件时初始化TCli和每次It结束后初始化的方法

    1
    2
    3
    4
    //在ginkgo bootstrap创建套件的文件中,表示启动测试套件的每一个总Describe运行前都要执行的代码
    var _ = BeforeSuite(func() {
    warpper.InitTCli()
    })
  • BeforeEachclient发送request和接收response的代码进行封装

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    func (t *TeClient) Do(method, url, reqJson string) error {
    adr := "http://" + ip + url
    t.Req, t.Err = http.NewRequest(method, adr, strings.NewReader(reqJson))
    t.Req.Header.Add("xxx", "xxx")
    t.Req.Header.Add("xxx", "xxx")
    if reqJson != "" {
    t.Req.Header.Add("Content-Type", "application/json")
    }

    t.Res, t.Err = t.Client.Do(t.Req)
    if t.Err != nil {
    err := t.Err
    return err
    }
    return nil
    }
    • 把测试组件相同的代码抽出成函数调用的方式,这样测试组件中的代码简介明了

    • Header.Add(“xxx”,"xxx")一般都是token、安全验证,因为在测试中使用的身份是一样的

    • 对于有传如request.BodyPOST方法才加Header.Add("Content-Type", "application/json")GET中不需要传入request.Body就不需要了


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!