gqty-dev / gqty

The No-GraphQL Client for TypeScript
https://gqty.dev
MIT License
921 stars 28 forks source link

gqty needs a dynamic mocks solution. #876

Open MarkLyck opened 2 years ago

MarkLyck commented 2 years ago

I really wanted to love gqty and use it for all of my projects.

Just writing code as if it was an object and have React Suspense load it is fantastic!

But ultimately I had to switch back to Apollo hooks due to the lack of mocking abilities with gqty.

I got so far as to have a fully dynamic mock system set up with gqty using @graphql-tools's mockServer to create a schema with mocks, and relying on my testing env variable to query the mockServer instead of using the regular queryFetcher in the generated gqty file.

It wasn't easy to set up, but it worked great! It would auto mock anything I wanted using faker with a seed for consistency in tests.

However in some tests you always need to override individual results and that's where gqty fell apart for me.

Since you don't define query names when using gqty I just had no way to reasonably identify a query.

My first thought was using types, but that didn't end up being feasible especially if the same type appeared multiple times in my component.

So I thought, I could serialize the query itself and use that as a key in my mockStore, which was "okay" for simple queries.

But when I started making even minor queries I quickly ended up with this mess:

mockQuery({
      query: {
        query: {
          crm: {
            __typename: true,
            sitesConnection0: {
              __typename: true,
              totalCount: true,
              __aliasFor: 'sitesConnection',
            },
            sitesConnection1: {
              __typename: true,
              totalCount: true,
              __aliasFor: 'sitesConnection',
              __args: { condition: { active: true } },
            },
            sites0: {
              __typename: true,
              id: true,
              __aliasFor: 'sites',
              __args: { first: 1, condition: { active: true } },
            },
          },
        },
      },
      result: {
        data: {
          crm: {
            __typename: 'crmQuery',
            sitesConnection0: {
              __typename: 'SitesConnection',
              totalCount: 14413,
            },
            sitesConnection1: {
              __typename: 'SitesConnection',
              totalCount: 12081,
            },
            sites0: [{ __typename: 'Site', id: 1 }],
          },
        },
      },
    })

This technically works... but no one wants to write or see that code in your test files just to override a graphql mock.

In other graphQL libraries you would usually assign a name to a query like this:

query SiteCounts {
  crm {
    sites: sitesConnection {
      totalCount
    }
    activeSites: sitesConnection(condition: { active: true }, first: 1) {
      totalCount
      nodes {
        id
      }
    }
  }
}

and then in e.g. Apollo you can mock it like so:

const mocks = {
      SiteCounts: {
        crm: {
          sitesConnection: {
            totalCount: 0,
          },
        },
      },
    }

    renderWithRouter(
      <AutoMockedProvider mocks={mocks}>
        <Dashboard />
      </AutoMockedProvider>
    )

But that method relies on the name I gave the query SiteCounts

I'm honestly not sure what a good solution for gqty would even look like. But hopefully this can start a discussion to find a way to implement a reasonable mocking system using gqty.

If it's any help this is the mockServer I created with gqty that "worked" with the serialized queries, but stringifying the query as the key is not useable.

import { mockServer, addMocksToSchema } from '@graphql-tools/mock'
import { makeExecutableSchema } from '@graphql-tools/schema'
import { graphQlQueryToJson } from 'graphql-query-to-json'
import schemaString from '../schema.graphql?raw'

import mocks from './mocks'

// mockStore
const mockStore = new Map()
type setMockType = { query: any; result: any }
export const mockQuery = ({ query, result }: setMockType) => {
  mockStore.set(String(query), result)
}
export const resetMocks = () => {
  mockStore.clear()
}

// mockServer
const schema = makeExecutableSchema({ typeDefs: schemaString })
const schemaWithMocks = addMocksToSchema({ schema, mocks })
const server = mockServer(schemaWithMocks, mocks)

const runQuery = (query: string, variables: any) => {
  // If there's an override in mockStore, return that.
  const queryKey = JSON.stringify(graphQlQueryToJson(query))
  if (mockStore.has(queryKey)) {
    return mockStore.get(queryKey)
  }

  return server.query(query, variables)
}

export default { query: runQuery }
MarkLyck commented 2 years ago

@vicary came up with the ideal solution to this problem on Discord.

This would be completely solved by adding operatioName to the useQuery.

like so:

const query = useQuery({ operationName: "SiteCounts" })

That would make it possible to easily override individual queries based on the operationName like we can with most other graphql clients 🎉

All credits to @vicary for the idea.

Remy-T commented 2 years ago

bump

vicary commented 1 year ago

@MarkLyck our new core is approaching beta from alpha. While we're updating our GitHub actions to properly release under the tag beta, please feel free to read our updated docs at https://gqty.dev and try it out!

The new useQuery and resolve supports operationName, you should be able to mock your queryFetcher like this:

const queryFetcher:QueryFetcher = async ({ query, variables, operationName }) => {
  if (operationName === 'MyTestQuery') {
    return {
      // mocked data
    };
  }
};