graphql-mocks / graphql-mocks

Build web applications today against the GraphQL APIs of tomorrow.
http://www.graphql-mocks.com
MIT License
60 stars 7 forks source link

Create a mocked GraphQL type directly #284

Open bengry opened 2 months ago

bengry commented 2 months ago

As detailed in this gist (also shared in https://github.com/graphql-mocks/graphql-mocks/issues/273), we're using graphql-mocks to create fake data for GQL requests.

Sometimes though we want to create a semi-fake type. i.e. create a basic type ourselves (similar to what DeepPartial<SomeGQLType> would do in TypeScript), and let graphql-mocks fill in the rest of the stuff.

Ideally something like:

import type { User } from 'path-to-generated-gql-types';

const mockedUser = createMockOf<User>('User', {
  name: {
    first: 'John'
  }
});

assert.equal(mockedUser, {
  name: {
    first: 'John',
    last: 'Doe', // mocked automatically,
  },
  address: {
    ...
  }
});

Is such a thing possible? The entire mocking infrastructure already exists, but I'm not sure if there's a userland API for connecting the dots and creating this createMockOf function.

chadian commented 1 month ago

That would make for a nice helper. Is there more to the example where mockedUser is being returned from a resolver and part of a graphql query?

One thing that makes it difficult, and maybe this doesn't matter in your case, is that with resolver functions you aren't creating individual User objects. Since first and last are individual resolver functions, if first always returned "John".

The query:

{
  users {
    first
    last
  }
}

would return

{
  "data": [
    { "first": "John", "last": "Smith" },
    { "first": "John", "last": "Johnson" }, 
  ]
}

That would look something like this:

import { setResolver } from 'graphql-mocks/resolver-map';
const createUserMockMiddleware = (hardcodedFirstName) => (resolverMap) => {
  setResolver(resolverMap, ["User", "first"], () => hardcodedFirstName);
  setResolver(resolverMap, ["User", "last"], () => faker.name());
}

If you wanted something less hardcoded and more representative of instances of users then decoupling the User from the resolver function to something stateful is helpful. Using graphql-paper comes in handy. This technique is better described in GraphQL Paper: Separation of Concrete and Derived Data. In this case the concrete data is first and the derived (dynamic) is last.

import { Paper } from "graphql-paper";

const paper = new Paper(graphqlSchema);

const userMiddleware = (resolverMap, { dependencies }) => {
  setReference(resolverMap, ['User', 'first'], (parent, args, context, info) =>
    // return a specific user based on some criteria
    dependencies.paper.User.find((user) => user.id === args.id)?.name
  );

  setReference(resolverMap, ['User', 'last'], () => sinon.name());
};

const handler = new GraphQLHandler({
  dependencies: {
    graphqlSchema,
    paper,
  },
});

// if the `async`
beforeEach(async () => {
  paper.mutate(({ create }) => {
    // create as many specific Users as needed
    create("User", { first: "John" });
    create("User", { first: "Jessica" });
  });
});

The last thing I'll add is that the default resolver for a field is to look at its parent and return the property that matches the field name, so if you have a schema like:

schema {
  query: Query
}

type Query {
  user: User
  users: [User!]!
}

type User {
  first: String
  last: String
}
const createMockuser = (partial) => {
  return {
    last: sinon.name(),
    ...partial,
  };
};

const resolverMap = {
  Query: {
    user: () => createMockUser({ first: "John" })
    users: () => [createMockUser({ first: "Bob" }), createMockUser({ first: "Jessica" })]
  }
};

This might be a simple way of doing what you're after.

I think GraphQL Paper: Separation of Concrete and Derived Data is probably the closest fit? Let me know if I'm missing something or if this isn't quite what you're after. It might be helpful to see the example as the assertion of mockedUser involving the resolvers and/or query.

bengry commented 1 month ago

@chadian I think that GraphQL Paper is the realm of what I'm describing, based on what I understand from the docs you linked, but its very specific. It seems like each function needs to be hand-crafted (as explained here). I think my use case overlaps with what Paper does, but I'm not sure it's exactly the same, or similar enough to use as-is.

What I want is a more generic solution - where I give it a GraphQL type + some data I pre-created, and it completes the rest with the same logic and middlewares that are already set on it.

e.g.

// @generated/graphqlTypes.ts
type DateTime = string;

interface Task {
  id: String;
  title: String;
  status: TaskStatus;
  completedAt: Maybe<DateTime>;
  createdBy: User;
}

interface User { ... }

enum TaskStatus {
  Pending = "PENDING",
  Completed = "COMPLETED",
  Wont_Do = "WONT_DO",
}

// tasks.spec.ts
test("Complete a task", ({ tasksPage, mockServer }) => {
  const pendingTask = createMockOf<Task>('Task', {
    status: TaskStatus.Pending, // this is the only pieces of information I care about for this test, the rest can be automatically mocked.
  });

  await mockServer.setupTasks([pendingTask]); // this sets up mocking the network request using msw or whatever

  await tasksPage.goto();

  await tasksPage.completeTask(task.title);

  expect(await tasksPage.findTask(task.title).isCompleted()).toBe(true);
});

What I want to happen behind the scenes when calling createMockOf is that graphql-mocks will run my Faker.js-based middleware and create a Task, along with any nested types it may include¹, to complete the missing fields.

¹ Note that in my example all fields are mocked, without taking the graphql query into account. i.e. even if createdBy is not requested by the GQL query on the page, pendingTask will have it. This may be further optimized, but I'm not sure how important it is to only generate the requested fields, in practical use.

My second issue with Paper etc., is the complexity from a consumer's PoV - it's hard (at least for me 😅) to understand what I need to set up, and how things connect to each other. At the end of the day I just want a way to mock GQL types and queries with custom logic and/or hand-picked values, per request. So ideally the API would be simple enough to use and set-up.

I hope I'm explaining myself well enough here, but LMK if you need more examples or use-cases. I'll see what I can derive from our code-base.