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

我正在构建一个简单的函数,该函数调用一个使用 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,当设置为空字符串时会出现失败情况,id这会导致下游调用出错c.gcl.Run。


我正在努力解决的是gcl客户端可以被模拟并强制返回错误的方式(当没有真正的 API 调用发生时)。

我不确定是否response像这样传递给模拟是正确的方法。此外,我正在努力为响应设置正确的值,因为interface{}传递了一个类型,但我不知道如何将其转换为getPostResponse并将值设置为Post那里。



慕容3067478
浏览 91回答 1
1回答

郎朗坤

您的测试用例不应超出实现范围。我特别指的是空对非空输入或任何类型的输入。让我们看一下您要测试的代码: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因此应该只有两个测试用例,第三个或第四个测试用例只是一个变体,如果不是一个完整的副本, 前两个。你的测试客户端也在做一些不必要的事情,主要的是它的名字,即你所拥有的不是一个模拟,所以调用它是没有帮助的。模拟通常不仅仅是返回预定义的值,它们确保方法被调用,以预期的顺序和预期的参数等。实际上你根本不需要模拟,所以这是一件好事'语气。考虑到这一点,我建议您对测试客户端执行以下操作。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)            }        })    }}...这里有一些重复,需要拼出postanderr两次,但与更复杂/复杂的测试设置相比,这是一个很小的代价,它将从测试用例的预期输出字段填充测试客户端.附录:如果您要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)            }        })    }}
打开App,查看更多内容
随时随地看视频慕课网APP

相关分类

Go