Как издеваться над вложенным клиентом в тесте - PullRequest
0 голосов
/ 25 апреля 2020

Я создаю простую функцию, которая вызывает API, который возвращает сообщение с использованием GraphQL (https://github.com/machinebox/graphql). Я обернул логи c в сервис, который выглядит следующим образом:

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 на макет таким образом, является правильным способом сделать это. Кроме того, я изо всех сил пытаюсь установить правильное значение для ответа, так как тип interface{} передан, и я не знаю, как преобразовать его в getPostResponse и установить там значение Post.

1 Ответ

1 голос
/ 25 апреля 2020

Ваши тестовые случаи не должны превышать go за пределами реализации. Я специально имею в виду пустой ввод-непустой ввод или любой другой тип ввода.

Давайте посмотрим на код, который вы хотите проверить:

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, и, следовательно, должно быть только два тестовых случая, 3-й или 4-й тестовый случай будет просто вариацией если не прямая копия первых двух.


Ваш тестовый клиент также делает некоторые ненужные вещи, основным из которых является его имя, т.е. это не полезно Обычно mocks делает намного больше, чем просто возвращает предопределенные значения, они гарантируют, что методы вызываются в ожидаемом порядке и с ожидаемыми аргументами и т. Д. c. И на самом деле здесь вам вообще не нужно издеваться, так что хорошо, что это не так.

Имея это в виду, вот что я бы посоветовал вам сделать с вашим тестовым клиентом.

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 таким образом, чтобы он проверял пустой идентификатор и возвращал ошибку перед отправкой запроса в 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)
            }
        })
    }
}
...