hasura / go-graphql-client

Package graphql provides a GraphQL client implementation.
MIT License
395 stars 91 forks source link

How to mock graphql.Client? #138

Open koote opened 3 months ago

koote commented 3 months ago

graphql.Client doesn't provide an interface, so how can I use gomock to create a mock of graphql.Client for testing? Thanks.

hgiasac commented 3 months ago

The graphql client is built upon a Doer interface. You can implement any mock client or use httptest client to intercept request and response data.

koote commented 3 months ago

@hgiasac That is too deep and too low level, means I need to manually create a graphql.Client based on a mock HTTP client, can we add an interface to graphql.Client so it can be mocked by using gomock?

hgiasac commented 3 months ago

I'm not sure about your needs. It's easy to write an interface for the graphql client For example, this is a simple wrapper interface for mock test

koote commented 3 months ago

The interface is what I am looking for, but I cannot use it because currently graphql.Client now is a struct not an interface, for example, this is my client code:

func Query(gqlClient *graphql.Client, some vars) error {
}

Because the function expects a object of graphql.Client, and it is a struct, I cannot pass in a mock implementation of the interface you shared.

Ideally, in my understanding, the graphql.Client should be an interface (like you shared), not a struct:

type Client interface {
    Query(ctx context.Context, q interface{}, variables map[string]interface{}, options ...graphql.Option) error
        ...
}

So when test my function Query, I can pass any object in, as long as it implements the Client interface.

However, currently the only way to test my code, is pass in a mock HTTP client when creating the graphql.Client, and I have to read and understand the code of graphql.Client so I can return appropriate http.Response object that graphql.Client expects, that is why I said only providing a Doer interface is too deep and too low level for anyone who is new to this library:

func Test(t *testing.T) {
    ctrl := gomock.NewController(t)
    defer ctrl.Finish()

    mockHTTPClient := mock.NewMockDoer(ctrl)
        mockHTTPClient.EXPECT().Do(gomock.Any()).Return(...).Times(1)
    gqlClient := graphql.NewClient(testServer.URL, mockHTTPClient)

       // continue testing of biz logic
}
hgiasac commented 3 months ago

The interface is what I am looking for, but I cannot use it because currently graphql.Client now is a struct not an interface, for example, this is my client code: Because the function expects a object of graphql.Client, and it is a struct, I cannot pass in a mock implementation of the interface you shared.

Can't you replace graphql.Client with the interface? Of course, you will need to modify your functions, but it's possible.

type Client interface {
    Query(ctx context.Context, q interface{}, variables map[string]interface{}, options ...graphql.Option) error
        ...
}

func Query(gqlClient Client, some vars) error {
}

To clarify this library is the fork of https://github.com/shurcooL/graphql. The client was originally designed with a struct, and we haven't planned to change that design. That will be a breaking change for current users.

koote commented 3 months ago

That means I need to define an interface and wrap the graphql.Client within my project, I am thinking it is a bit not clean solution. But finally I managed to write my test in this way:

func Test_QueryAccounts(t *testing.T) {
        filter := getDefaultFilter()
    mux := http.NewServeMux()
    mux.HandleFunc("/graphql", func(writer http.ResponseWriter, request *http.Request) {
        body, err := io.ReadAll(request.Body)
        require.NoError(t, err)
        require.Equal(t, fmt.Sprintf(accountQueryBody, filter), string(body))

        queryResults, err := json.Marshal(testAccounts)
        require.NoError(t, err)

        _, err = io.WriteString(writer, fmt.Sprintf(accountQueryResponseWithoutError, queryResults))
        require.NoError(t, err)

        writer.Header().Set("Content-Type", "application/json")
    })
    testServer := httptest.NewServer(mux)
    defer testServer.Close()

    myClient, err := NewClient(&Config{Endpoint: testServer.URL + "/graphql"}, WithHTTPClient(http.DefaultClient))
    require.NoError(t, err)
    require.NotNil(t, myClient)

    accounts, err := myClient.QueryAccounts(context.Background(), filter)
    require.NoError(t, err)
    require.Equal(t, len(testAccounts), len(accounts))
}