我正在为Elasticsearch中间件编写测试,其中我正在使用一个函数来构建测试服务器,在其中我为每个测试传递一个配置结构片,并在处理程序函数中对其进行迭代,并将预期的响应写入其中。响应作者。这是我的职责。
func newMockClient(url string) (*elasticsearch, error) {
client, err := elastic.NewSimpleClient(elastic.SetURL(url))
if err != nil {
return nil, fmt.Errorf("error while initializing elastic client: %v", err)
}
es := &elasticsearch{
url: url,
client: client,
}
return es, nil
}
type ServerSetup struct {
Method, Path, Body, Response string
HTTPStatus int
}
func buildTestServer(t *testing.T, setups []*ServerSetup) *httptest.Server {
handlerFunc := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
requestBytes, _ := ioutil.ReadAll(r.Body)
requestBody := string(requestBytes)
matched := false
for _, setup := range setups {
if r.Method == setup.Method && r.URL.EscapedPath() == setup.Path {
matched = true
if setup.HTTPStatus == 0 {
w.WriteHeader(http.StatusOK)
} else {
w.WriteHeader(setup.HTTPStatus)
}
_, err := w.Write([]byte(setup.Response))
if err != nil {
t.Fatalf("Unable to write test server response: %v", err)
}
}
}
if !matched {
t.Fatalf("No requests matched setup. Got method %s, Path %s, body %s", r.Method, r.URL.EscapedPath(), requestBody)
}
})
return httptest.NewServer(handlerFunc)
}
它是从github.com/github/vulcanizer
复制而来的。当我使用它运行单个测试时,它可以正常工作。例如这个测试
func TestCreateIndex(t *testing.T) {
setup := &ServerSetup{
Method: "PUT",
Path: "/test",
Response: `{"acknowledged": true, "shards_acknowledged": true, "index": "test"}`,
}
ts := buildTestServer(t, []*ServerSetup{setup})
es, _ := newMockClient(ts.URL)
err := es.createIndex(context.Background(), "test", nil)
if err != nil {
t.Fatalf("Index creation failed with error: %v\n", err)
}
}
但是当我尝试在单个测试中检查这种不同的行为时,会出现错误http: multiple response.WriteHeader calls
func TestDeleteIndex(t *testing.T) {
setup := &ServerSetup{
Method: "DELETE",
Path: "/test",
Response: `{"acknowledged": true}`,
}
errSetup := &ServerSetup{
Method: "DELETE",
Path: "/test",
Response: `{"acknowledged": false}`,
}
ctx := context.Background()
ts := buildTestServer(t, []*ServerSetup{setup, errSetup})
defer ts.Close()
es, _ := newMockClient(ts.URL)
err := es.deleteIndex(ctx, "test")
if err != nil {
t.Fatalf("Index creation failed with error: %v\n", err)
}
err = es.deleteIndex(ctx, "test")
if err == nil {
t.Fatal("Expected error but not found")
}
}
我猜测是因为这样的事实,当我第二次运行deleteIndex时,它将再次对服务器执行ping操作,但是响应编写器已经被写入,因此它无法再写入任何内容。
无论如何,我可以在处理程序函数的开头进行检查,例如
handlerFunc := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if w != nil{
// clear data in response writer
}
// .........
}
答案 0 :(得分:3)
我认为您正在做的事情不是测试功能的正确方法。您需要将测试与测试用例分开,以检查诸如此类的不同行为:
func Test_DeleteIndex(t *testing.T) {
t.Run("Should be ok with correct setup", func(t *testing.T) {
setup := &ServerSetup{
Method: "DELETE",
Path: "/test",
Response: `{"acknowledged": true}`,
}
ctx := context.Background()
ts := buildTestServer(t, []*ServerSetup{setup})
defer ts.Close()
es, _ := newMockClient(ts.URL)
err := es.deleteIndex(ctx, "test")
require.NoError(t, err)
})
t.Run("Shouldn't be ok with wrong setup", func(t *testing.T) {
setup := &ServerSetup{
Method: "DELETE",
Path: "/test",
Response: `{"acknowledged": false}`,
}
ctx := context.Background()
ts := buildTestServer(t, []*ServerSetup{setup})
defer ts.Close()
es, _ := newMockClient(ts.URL)
err := es.deleteIndex(ctx, "test")
require.Error(t, err)
})
}
答案 1 :(得分:2)
这里的问题是,对于测试服务器获得的每个请求,处理程序都会根据方法和路径遍历所有ServerSetup
结构以检查是否存在匹配,但不会在找到匹配项时脱离循环
因此,在第二个测试用例中,由于您传递了两个具有相同Method
和Path
的安装结构,因此两个安装例将匹配对DELETE /test
的传入请求,并且程序将尝试两次在WriteHeader
上致电ResponseWriter
。
我可以通过两种方式解决此问题:
选项1
如果您希望服务器对相同方法和路径组合的连续调用做出不同的响应,则可以添加一个属性来检查ServerSetup
实例是否已被使用,并避免使用任何具有已经被使用。
例如:
type ServerSetup struct {
Method, Path, Body, Response string
HTTPStatus int
HasBeenCalled bool
}
func buildTestServer(t *testing.T, setups []*ServerSetup) *httptest.Server {
handlerFunc := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
requestBytes, _ := ioutil.ReadAll(r.Body)
requestBody := string(requestBytes)
matched := false
for _, setup := range setups {
if setup.HasBeenCalled {
continue // skip those that have already been called
}
if r.Method == setup.Method && r.URL.EscapedPath() == setup.Path {
setup.HasBeenCalled = true
matched = true
if setup.HTTPStatus == 0 {
w.WriteHeader(http.StatusOK)
} else {
w.WriteHeader(setup.HTTPStatus)
}
_, err := w.Write([]byte(setup.Response))
if err != nil {
t.Fatalf("Unable to write test server response: %v", err)
}
}
if matched {
break // stop checking for matches if already found match
}
}
if !matched {
t.Fatalf("No requests matched setup. Got method %s, Path %s, body %s", r.Method, r.URL.EscapedPath(), requestBody)
}
})
return httptest.NewServer(handlerFunc)
}
选项2
解决此问题的一种稍微简单的方法是为这两种情况分别创建测试服务器,为每种setup
结构创建一个测试服务器,因为它们涉及相同方法和路径组合的不同结果。更清晰地说,您可以将它们分为两个单独的测试。
所以您最终会得到:
func TestDeleteIndex_Success(t *testing.T) {
setup := &ServerSetup{
Method: "DELETE",
Path: "/test",
Response: `{"acknowledged": true}`,
}
ctx := context.Background()
ts := buildTestServer(t, []*ServerSetup{setup})
defer ts.Close()
es, _ := newMockClient(ts.URL)
err := es.deleteIndex(ctx, "test")
if err != nil {
t.Fatalf("Index creation failed with error: %v\n", err)
}
}
func TestDeleteIndex_Error(t *testing.T) {
errSetup := &ServerSetup{
Method: "DELETE",
Path: "/test",
Response: `{"acknowledged": false}`,
}
ctx := context.Background()
ts := buildTestServer(t, []*ServerSetup{errSetup})
defer ts.Close()
es, _ := newMockClient(ts.URL)
err := es.deleteIndex(ctx, "test")
if err == nil {
t.Fatal("Expected error but not found")
}
}
您将来也应该避免以后使用相同的方法路径组合传递多个ServerSetup
结构,以免发生此错误。