如何在测试中模拟嵌套客户端

时间:2020-04-25 11:25:47

标签: unit-testing go

我正在构建一个简单的函数,该函数调用使用GraphQL(https://github.com/machinebox/graphql)返回Post的API。我将逻辑包装在一个看起来像这样的服务中:

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函数添加test tables,但当id设置为空字符串时会失败,这会导致下游调用{{1} }。

我正在苦苦挣扎的是c.gcl.Run客户端可以被模拟并被强制返回错误的方式(当没有真正的API调用发生时)。

到目前为止我的测试:

gcl

我不确定像这样将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类型,而且我不知道如何将其转换为interface{}并将值设置为{{1, }}。

1 个答案:

答案 0 :(得分:1)

您的测试用例不应超出实现范围。我专门指的是empty-vs-nonempty输入或实际上是任何一种输入。

让我们看一下您要测试的代码:

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变量的“零”)的代码分支。这意味着,就您的实现而言,根据errRun返回的值,只有两种可能的结果,因此应该只有两个测试用例,第三个或第四个测试用例只是前两个版本的变体,甚至不是完整版本。


您的测试客户端也做了一些不必要的事情,主要是它的名字,即您所拥有的没有模拟程序,因此称它为无益。 Mocks通常做的不只是返回预定义的值,它们还可以确保以预期的顺序和预期的参数等方式调用方法。实际上,您根本不需要模拟,所以这是一件好事不是一个。

请记住,这是我建议您与测试客户一起做的事情。

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

...这里有些重复,需要两次拼写posterr,但是与更复杂/更复杂的测试设置相比,这是一个很小的代价将从测试用例的预期输出字段填充测试客户端。


附录:

如果您要更新GetPost,使其在向graphql发送请求之前检查空ID并返回错误,则您的初始设置会更有意义:

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