简介

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

复制代码然后执行

1
go test -v -cover ...

使用这个包,我们可以对 restful 的每个 api 都进行测试 ,更大程度地提升了代码质量

参考:https://juejin.im/post/5ab9fb50518825557459b94d