我正在构建一个简单的函数,该函数调用一个API,使用GraphQL返回一个Post (https:/github.commachineboxgraphql。). 我把逻辑封装在一个服务中,看起来像这样。
type Client struct {
gcl graphqlClient
}
type graphqlClient interface {
Run(ctx context.Context, req *graphql.Request, resp interface{}) error
}
func (c *Client) GetPost(id string) (*Post, error) {
req := graphql.NewRequest(`
query($id: String!) {
getPost(id: $id) {
id
title
}
}
`)
req.Var("id", id)
var resp getPostResponse
if err := c.gcl.Run(ctx, req, &resp); err != nil {
return nil, err
}
return resp.Post, nil
}
现在我想添加 测试表 对于 GetPost
时,函数会出现失败的情况。id
被设置为空字符串,这将导致下游调用的错误。c.gcl.Run
.
我所苦恼的是,我的方式是 gcl
客户端可以被嘲讽并强制返回错误(当没有发生真正的API调用时)。
到目前为止,我的测试。
package apiClient
import (
"context"
"errors"
"github.com/aws/aws-sdk-go/aws"
"github.com/google/go-cmp/cmp"
"github.com/machinebox/graphql"
"testing"
)
type graphqlClientMock struct {
graphqlClient
HasError bool
Response interface{}
}
func (g graphqlClientMock) Run(_ context.Context, _ *graphql.Request, response interface{}) error {
if g.HasError {
return errors.New("")
}
response = g.Response
return nil
}
func newTestClient(hasError bool, response interface{}) *Client {
return &Client{
gcl: graphqlClientMock{
HasError: hasError,
Response: response,
},
}
}
func TestClient_GetPost(t *testing.T) {
tt := []struct{
name string
id string
post *Post
hasError bool
response getPostResponse
}{
{
name: "empty id",
id: "",
post: nil,
hasError: true,
},
{
name: "existing post",
id: "123",
post: &Post{id: aws.String("123")},
response: getPostResponse{
Post: &Post{id: aws.String("123")},
},
},
}
for _, tc := range tt {
t.Run(tc.name, func(t *testing.T) {
client := newTestClient(tc.hasError, tc.response)
post, err := client.GetPost(tc.id)
if err != nil {
if tc.hasError == false {
t.Error("unexpected error")
}
} else {
if tc.hasError == true {
t.Error("expected error")
}
if cmp.Equal(post, &tc.post) == false {
t.Errorf("Response data do not match: %s", cmp.Diff(post, tc.post))
}
}
})
}
}
我不确定是否通过 response
到mock这样的方式才是正确的。另外,我正在努力为响应设置正确的值,因为一个 interface{}
类型,但我不知道如何将其转换为 getPostResponse
并将该值设置为 Post
在那里,我正在构建一个简单的函数,该函数调用API,使用GraphQL返回一个Post(https:/github.commachineboxgraphql)。
你的测试用例 不该 超越了执行的范围,我特别指的是空与非空输入或任何一种输入。我特别指的是空与非空输入或任何一种输入真的。
让我们来看看你要测试的代码。
func (c *Client) GetPost(id string) (*Post, error) {
req := graphql.NewRequest(`
query($id: String!) {
getPost(id: $id) {
id
title
}
}
`)
req.Var("id", id)
var resp getPostResponse
if err := c.gcl.Run(ctx, req, &resp); err != nil {
return nil, err
}
return resp.Post, nil
}
上面的实现中没有任何东西是基于 id
参数值,因此在你对这段代码的测试中,没有任何东西应该真正关心传递进来的是什么输入,如果它与实现无关,也应该与测试无关。
你的 GetPost
基本上有两个代码分支,这些分支是基于一个因素,即返回的 err
变量。这意味着,就你的实施而言,只有两种可能的结果,基于你的 err
价值 Run
返回,因此应该只有两个测试用例,第三个或第四个测试用例只是前两个测试用例的变体,如果不是完全复制的话。
你的测试客户端还做了一些不必要的事情,主要是它的名字,也就是说你那里的不是一个mock,所以叫它这个名字是没有用的。Mock通常做的事情不仅仅是返回预定义的值,它们确保方法被调用,按照预期的顺序和预期的参数等。而实际上你在这里根本不需要mock,所以它不是一个好东西。
考虑到这一点,这里我建议你用你的测试客户端来做。
type testGraphqlClient struct {
resp interface{} // non-pointer value of the desired response, or nil
err error // the error to be returned by Run, or nil
}
func (g testGraphqlClient) Run(_ context.Context, _ *graphql.Request, resp interface{}) error {
if g.err != nil {
return g.err
}
if g.resp != nil {
// use reflection to set the passed in response value
// (i haven't tested this so there may be a bug or two)
reflect.ValueOf(resp).Elem().Set(reflect.ValueOf(g.resp))
}
return nil
}
......这里是必要的测试用例,全部两个。
func TestClient_GetPost(t *testing.T) {
tests := []struct {
name string
post *Post
err error
client testGraphqlClient
}{{
name: "return error from client",
err: errors.New("bad input"),
client: testGraphqlClient{err: errors.New("bad input")},
}, {
name: "return post from client",
post: &Post{id: aws.String("123")},
client: testGraphqlClient{resp: getPostResponse{Post: &Post{id: aws.String("123")}}},
}}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
client := Client{gql: tt.client}
post, err := client.GetPost("whatever")
if !cmp.Equal(err, tt.err) {
t.Errorf("got error=%v want error=%v", err, tt.err)
}
if !cmp.Equal(post, tt.post) {
t.Errorf("got post=%v want post=%v", post, tt.post)
}
})
}
}
...这里有一点重复的地方,需要写出... ... post
和 err
两次,但与更复杂的测试设置相比,这只是一个很小的代价,因为测试设置会从测试用例的预期输出字段中填充测试客户端。
补遗。
如果你要更新 GetPost
在这样的方式,它检查空的ID,并返回一个错误之前,它发送一个请求到graphql,那么你的初始设置将更有意义。
func (c *Client) GetPost(id string) (*Post, error) {
if id == "" {
return nil, errors.New("empty id")
}
req := graphql.NewRequest(`
query($id: String!) {
getPost(id: $id) {
id
title
}
}
`)
req.Var("id", id)
var resp getPostResponse
if err := c.gcl.Run(ctx, req, &resp); err != nil {
return nil, err
}
return resp.Post, nil
}
...并相应地更新测试用例。
func TestClient_GetPost(t *testing.T) {
tests := []struct {
name string
id string
post *Post
err error
client testGraphqlClient
}{{
name: "return empty id error",
id: "",
err: errors.New("empty id"),
client: testGraphqlClient{},
}, {
name: "return error from client",
id: "nonemptyid",
err: errors.New("bad input"),
client: testGraphqlClient{err: errors.New("bad input")},
}, {
name: "return post from client",
id: "nonemptyid",
post: &Post{id: aws.String("123")},
client: testGraphqlClient{resp: getPostResponse{Post: &Post{id: aws.String("123")}}},
}}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
client := Client{gql: tt.client}
post, err := client.GetPost(tt.id)
if !cmp.Equal(err, tt.err) {
t.Errorf("got error=%v want error=%v", err, tt.err)
}
if !cmp.Equal(post, tt.post) {
t.Errorf("got post=%v want post=%v", post, tt.post)
}
})
}
}