简介
httpexpect基本上,是一组基于HTTP请求和基于HTTP的断言( 例如HTTP响应和负载),位于net/HTTP和几个实用程序包之上的。
工作流:
- 增量生成HTTP请求。
- 检查HTTP响应。
- 递归检查响应负载。
特性
- 请求生成器
- URL路径构造,由 go-interpol 包提供的简单字符串内插。
- URL查询参数( 使用 go-querystring 封装进行编码)。
- 标题,Cookies,有效负载:JSON,urlencoded或者多部分表单( 使用 form 封装进行编码),纯文本。
- 创建可以重用的自定义请求生成器插件。
- 响应断言
- 响应状态,预定义状态范围。
- 标题,Cookies,有效负载:JSON,JSONP,窗体,文本。
- 往返时间。
- 负载断言
- 键入特定的断言,受支持的类型:对象,array,字符串,数字,布尔值,空值,日期时间。
- 正规表达式。
- 简单JSON查询( 使用 JSONPath的子集),由jsonpath 包提供。
- 由 gojsonschema 包提供的 JSON架构验证。
- 打印精美
- 详细错误消息。
- JSON差异是在使用 gojsondiff 包失败时产生的。
- 使用 testify (。assert 或者 require 软件包) 或者标准 testing 包报告故障。
- 使用 httputil, http2curl 或者简单 compact 记录器将请求和响应转储为各种格式。
- 调优
- 测试可以通过实际的HTTP客户端与服务器通信,或者直接调用 net/http 或者 fasthttp。
- 用户可以提供自定义HTTP客户端。记录器。打印机和故障报告器。
- 可以提供自定义HTTP请求工厂,从 Google App Engine 测试中提供 比如。
使用方法
Hello, world!
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
|
package example
import (
"net/http"
"net/http/httptest"
"testing"
"github.com/gavv/httpexpect"
)
func TestFruits(t *testing.T) {
// create http.Handler
handler := FruitsHandler()
// run server using httptest
server := httptest.NewServer(handler)
defer server.Close()
// create httpexpect instance
e := httpexpect.New(t, server.URL)
// is it working?
e.GET("/fruits").
Expect().
Status(http.StatusOK).JSON().Array().Empty()
}
|
JSON
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
|
orange := map[string]interface{}{
"weight": 100,
}
// PUT 创建一个橘子
e.PUT("/fruits/orange").WithJSON(orange).
Expect().
Status(http.StatusNoContent).NoContent()
// GET 然后获取, 并校验数据中是否含有 weight: 100
e.GET("/fruits/orange").
Expect().
Status(http.StatusOK).
JSON().Object().ContainsKey("weight").ValueEqual("weight", 100)
apple := map[string]interface{}{
"colors": []interface{}{"green", "red"},
"weight": 200,
}
// 创建一个苹果
e.PUT("/fruits/apple").WithJSON(apple).
Expect().
Status(http.StatusNoContent).NoContent()
// 获取这个苹果
obj := e.GET("/fruits/apple").
Expect().
Status(http.StatusOK).JSON().Object()
obj.Keys().ContainsOnly("colors", "weight")
// 对 返回数据逐一测试
obj.Value("colors").Array().Elements("green", "red")
obj.Value("colors").Array().Element(0).String().Equal("green")
obj.Value("colors").Array().Element(1).String().Equal("red")
obj.Value("colors").Array().First().String().Equal("green")
obj.Value("colors").Array().Last().String().Equal("red")
|
JSON Schema and JSON Path
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
schema := `{
"type": "array",
"items": {
"type": "object",
"properties": {
...
"private": {
"type": "boolean"
}
}
}
}`
repos := e.GET("/repos/octocat").
Expect().
Status(http.StatusOK).JSON()
// validate JSON schema
repos.Schema(schema)
// run JSONPath query and iterate results
for _, private := range repos.Path("$..private").Array().Iter() {
private.Boolean().False()
}
|
Forms
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
// post form encoded from struct or map
e.POST("/form").WithForm(structOrMap).
Expect().
Status(http.StatusOK)
// set individual fields
e.POST("/form").WithFormField("foo", "hello").WithFormField("bar", 123).
Expect().
Status(http.StatusOK)
// multipart form
e.POST("/form").WithMultipart().
WithFile("avatar", "./john.png").WithFormField("username", "john").
Expect().
Status(http.StatusOK)
|
URL construction
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
// construct path using ordered parameters
e.GET("/repos/{user}/{repo}", "octocat", "hello-world").
Expect().
Status(http.StatusOK)
// construct path using named parameters
e.GET("/repos/{user}/{repo}").
WithPath("user", "octocat").WithPath("repo", "hello-world").
Expect().
Status(http.StatusOK)
// set query parameters
e.GET("/repos/{user}", "octocat").WithQuery("sort", "asc").
Expect().
Status(http.StatusOK) // "/repos/octocat?sort=asc"
|
Headers
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
// set If-Match
e.POST("/users/john").WithHeader("If-Match", etag).WithJSON(john).
Expect().
Status(http.StatusOK)
// check ETag
e.GET("/users/john").
Expect().
Status(http.StatusOK).Header("ETag").NotEmpty()
// check Date
t := time.Now()
e.GET("/users/john").
Expect().
Status(http.StatusOK).Header("Date").DateTime().InRange(t, time.Now())
|
Cookies
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
// set cookie
t := time.Now()
e.POST("/users/john").WithCookie("session", sessionID).WithJSON(john).
Expect().
Status(http.StatusOK)
// check cookies
c := e.GET("/users/john").
Expect().
Status(http.StatusOK).Cookie("session")
c.Value().Equal(sessionID)
c.Domain().Equal("example.com")
c.Path().Equal("/")
c.Expires().InRange(t, t.Add(time.Hour * 24))
|
Regular expressions
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
// simple match
e.GET("/users/john").
Expect().
Header("Location").
Match("http://(.+)/users/(.+)").Values("example.com", "john")
// check capture groups by index or name
m := e.GET("/users/john").
Expect().
Header("Location").Match("http://(?P<host>.+)/users/(?P<user>.+)")
m.Index(0).Equal("http://example.com/users/john")
m.Index(1).Equal("example.com")
m.Index(2).Equal("john")
m.Name("host").Equal("example.com")
m.Name("user").Equal("john")
|
Subdomains and per-request URL
1
2
3
4
5
6
7
|
e.GET("/path").WithURL("http://example.com").
Expect().
Status(http.StatusOK)
e.GET("/path").WithURL("http://subdomain.example.com").
Expect().
Status(http.StatusOK)
|
WebSocket support
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
|
ws := e.GET("/mysocket").WithWebsocketUpgrade().
Expect().
Status(http.StatusSwitchingProtocols).
Websocket()
defer ws.Disconnect()
ws.WriteText("some request").
Expect().
TextMessage().Body().Equal("some response")
ws.CloseWithText("bye").
Expect().
CloseMessage().NoContent()
Reusable builders
e := httpexpect.New(t, "http://example.com")
r := e.POST("/login").WithForm(Login{"ford", "betelgeuse7"}).
Expect().
Status(http.StatusOK).JSON().Object()
token := r.Value("token").String().Raw()
auth := e.Builder(func (req *httpexpect.Request) {
req.WithHeader("Authorization", "Bearer "+token)
})
auth.GET("/restricted").
Expect().
Status(http.StatusOK)
e.GET("/restricted").
Expect().
Status(http.StatusUnauthorized)
|
Reusable matchers
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
e := httpexpect.New(t, "http://example.com")
// every response should have this header
m := e.Matcher(func (resp *httpexpect.Response) {
resp.Header("API-Version").NotEmpty()
})
m.GET("/some-path").
Expect().
Status(http.StatusOK)
m.GET("/bad-path").
Expect().
Status(http.StatusNotFound)
|
Custom config
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
e := httpexpect.WithConfig(httpexpect.Config{
// prepend this url to all requests
BaseURL: "http://example.com",
// use http.Client with a cookie jar and timeout
Client: &http.Client{
Jar: httpexpect.NewJar(),
Timeout: time.Second * 30,
},
// use fatal failures
Reporter: httpexpect.NewRequireReporter(t),
// use verbose logging
Printers: []httpexpect.Printer{
httpexpect.NewCurlPrinter(t),
httpexpect.NewDebugPrinter(t, true),
},
})
|
Session support
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
// cookie jar is used to store cookies from server
e := httpexpect.WithConfig(httpexpect.Config{
Reporter: httpexpect.NewAssertReporter(t),
Client: &http.Client{
Jar: httpexpect.NewJar(), // used by default if Client is nil
},
})
// cookies are disabled
e := httpexpect.WithConfig(httpexpect.Config{
Reporter: httpexpect.NewAssertReporter(t),
Client: &http.Client{
Jar: nil,
},
})
|
Use HTTP handler directly
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
|
// invoke http.Handler directly using httpexpect.Binder
var handler http.Handler = MyHandler()
e := httpexpect.WithConfig(httpexpect.Config{
Reporter: httpexpect.NewAssertReporter(t),
Client: &http.Client{
Transport: httpexpect.NewBinder(handler),
Jar: httpexpect.NewJar(),
},
})
// invoke fasthttp.RequestHandler directly using httpexpect.FastBinder
var handler fasthttp.RequestHandler = myHandler()
e := httpexpect.WithConfig(httpexpect.Config{
Reporter: httpexpect.NewAssertReporter(t),
Client: &http.Client{
Transport: httpexpect.NewFastBinder(handler),
Jar: httpexpect.NewJar(),
},
})
Per-request client or handler
e := httpexpect.New(t, server.URL)
client := &http.Client{
Transport: &http.Transport{
DisableCompression: true,
},
}
// overwrite client
e.GET("/path").WithClient(client).
Expect().
Status(http.StatusOK)
// construct client that invokes a handler directly and overwrite client
e.GET("/path").WithHandler(handler).
Expect().
Status(http.StatusOK)
|
与Gin结合
下面是一个依赖 gin 框架 api 项目使用 httpexpect 的例子。
app.go:
1
2
3
4
5
6
7
8
9
|
package main
import (
"./engine"
)
func main() {
engine.GetMainEngine().Run(":4000")
}
|
engine/engine.go:
这里之所以多一个 engine.go, 是因为我们要把 *gin.Engine 返回给 httpexpect,创建 server.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
package engine
import (
"github.com/gin-contrib/sessions"
"github.com/gin-gonic/gin"
)
func GetMainEngine() *gin.Engine {
r := gin.New()
// db, store := database.Connect()
// logdb := database.ConnectLog()
// r.Use(sessions.Sessions("xxx", store))
// r.Use(corsMiddleware())
// r.Use(gin.Logger())
// r.Use(gin.Recovery())
// r.Use(requestLogger())
// 一堆自定义的 handler
routers.Init(r)
return r
}
|
articles_test.go:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
package test
var eng *httpexpect.Expect
func GetEngine(t *testing.T) *httpexpect.Expect {
gin.SetMode(gin.TestMode)
if eng == nil {
server := httptest.NewServer(engine.GetMainEngine())
eng = httpexpect.New(t, server.URL)
}
return eng
}
func TestArticles(t *testing.T) {
e := GetEngine(t)
e.GET("/api/v1/articles").
Expect().
Status(http.StatusOK).
JSON().Object().ContainsKey("data").Keys().Length().Ge(0)
}
|
复制代码然后执行
使用这个包,我们可以对 restful 的每个 api 都进行测试 ,更大程度地提升了代码质量
参考:https://juejin.im/post/5ab9fb50518825557459b94d