Download as pdf or txt
Download as pdf or txt
You are on page 1of 48

Sophisticated Schema Mocking

Stephanie Saunders, Coinbase

#GraphQLConf
(2.5 yrs)
IC => Engineering Manager (5 mo)
● Developer Experience Org (velocity and quality)
○ Customers == Coinbase Engineers
● Data Layer Team (full stack)
○ GraphQL Service and Schema
○ @cbhq/data-layer
■ Wrapper around our GraphQL client
● Developer Experience Org (velocity and quality)
○ Customers == Coinbase Engineers
● Data Layer Team (full stack)
○ GraphQL Service and Schema
○ @cbhq/data-layer
■ Wrapper around our GraphQL client
● Migration from REST to GraphQL
○ Defining tools and best practices
○ Biggest customer complaint:
■ Mocking GraphQL in tests (Unit/E2E)
import { useLazyLoadQuery } from '@cbhq/data-layer';

const { viewer } = useLazyLoadQuery<UserPropertiesQuery>(


graphql`
query UserPropertiesQuery {
viewer {
userProperties {
name
}
}
}
`,
{},
);
it('renders', async () => {
const scope = nock(TEST_GRAPHQL_URL)
.post(TEST_GRAPHQL_PATH_MATCHER)
.reply(200, {
data: {
viewer: {
id: 'test-viewer-id',
userProperties: {
id: 'test-user-properties-id',
name: 'test-user-name',
},
},
},
});

const environment = await createTestEnvironment({});


const { findByTestId } = render(<TestFixture environment={environment} />);
expect(await findByTestId('user-properties')).toBeVisible();
scope.done();
});
const { viewer } = useLazyLoadQuery<UserPropertiesQuery>(
graphql`
query UserPropertiesQuery {
viewer {
userProperties {
name
regionFeatures {
isInUs
}
}
}
}
`,
{},
);
const { viewer } = useLazyLoadQuery<UserPropertiesQuery>(
graphql`
query UserPropertiesQuery {
viewer {
userProperties {
name
regionFeatures {
isInUs
}
}
}
}
`,
{},
);
it('renders', async () => {
const scope = nock(TEST_GRAPHQL_URL)
.post(TEST_GRAPHQL_PATH_MATCHER)
.reply(200, {
data: {
viewer: {
id: 'test-viewer-id',
userProperties: {
id: 'test-user-properties-id',
name: 'test-user-name',
},
regionFeatures: {
isInUs: true,
},
},
},
});
const environment = await createTestEnvironment({});
const { findByTestId } = render(<TestFixture environment={environment} />);
expect(await findByTestId('user-properties')).toBeVisible();
scope.done();
});
import { graphql } from 'msw';

export function createHandlers(): (


| RestHandler<MockedRequest<DefaultBodyType>>
| GraphQLHandler<GraphQLRequest<GraphQLVariables>>
)[] {
return [
graphql.query('UserPropertiesQuery', (req, res, ctx) => {
let testName = 'test-user-name';

if (req.variables?.id === 2) {
testName = 'test-user-name-2';
}

return res(
ctx.data({
viewer: {
id: 'test-viewer-id',
userProperties: {
id: 'test-user-properties-id',
name: testName,
},
const server = createTestMockServiceWorker(...);

beforeAll(() => server.listen());

afterEach(() => server.resetHandlers());

afterAll(() => server.close());


import { MockedProvider } from "@apollo/client/testing";

render(
<MockedProvider mocks={mocks} addTypename={false}>
<UserProperties />
</MockedProvider>
);
const mocks = [
{
request: {
query: UserPropertiesQuery,
variables: {
id: 42
}
},
result: {
data: {
viewer: {
id: 'test-viewer-id',
userProperties: {
id: 'test-user-properties-id',
name: 'test-user-name',
},
},
},
}
}
];
relayEnvironment.mock.queueOperationResolver((operation) => {
return MockPayloadGenerator.generate(operation);
});
const { viewer } = useLazyLoadQuery<UserPropertiesQuery>(
graphql`
query UserPropertiesQuery {
viewer {
userProperties {
name
regionFeatures {
isInUs
}
}
}
}
`,
{},
);
const { viewer } = useLazyLoadQuery<UserPropertiesQuery>(
graphql`
query UserPropertiesQuery {
viewer {
userProperties {
name
regionFeatures {
isInUs
}
createdAt
}
}
}
`,
{},
);

const date = formatDate(viewer.userProperties.createdAt);


relayEnvironment.mock.queueOperationResolver((operation) => {
return MockPayloadGenerator.generate(operation, {
UserProperties() {
return {
createdAt: '2021-09-01T00:00:00.000Z',
};
},
});
});
const otherData = useGetOtherData(); // hook with its own query

const { viewer } = useLazyLoadQuery<UserPropertiesQuery>(


graphql`
query UserPropertiesQuery {
viewer {
userProperties {
name
regionFeatures {
isInUs
}
createdAt
}
}
}
`,
{},
);

const date = formatDate(viewer.userProperties.createdAt);


relayEnvironment.mock.queueOperationResolver((operation) => {
return MockPayloadGenerator.generate(operation, {
UserProperties() {
return {
createdAt: '2021-09-01T00:00:00.000Z',
};
},
});
});

relayEnvironment.mock.queueOperationResolver((operation) => {
return MockPayloadGenerator.generate(operation);
});
● Recap - Common Issues
○ Nock
■ Static mocks
■ No easy access to query variables
○ MSW
■ Static mocks
■ No React Native support
■ Extra config
○ Apollo
■ Static mocks (even variables)
○ Relay
■ Dynamic mocks only return strings
■ Have to mock operations in order
What we need:
● No mocking required just to render
○ Adding queries/fields to a component does
not break everything
● Clear access to query variables
○ And all request data
● Test should be easy to set up, with very little code
import { faker } from '@faker-js/faker';

export const defaultMocks = {


// scalars
String: () => 'mock-value-for-string',
Boolean: () => true,
Int: () => 1,
Float: () => '2.42',

// custom scalars
Uuid: () => faker.datatype.uuid(),
Email: () => 'default.user@coinbase.com',
Date: () => '2022-11-12',
Time: () => '2022-11-12T16:06:15.442Z',
Decimal: () => '42.42',
Url: () => 'http://testUrl.com',
TickerSymbol: () => 'USD',
CountryCode: () => 'US',
Json: () => {},
Locale: () => 'en',
PhoneNumber: () => '4152564879',
return createGraphqlEnvironment({
. . .
interceptors: {
queryLevelMockerConfig: {
queryLevelMockerMiddleware: createQueryLevelMockerMiddleware({
schema,
mockEnums,
mockOverrides,
mockResolvers,
baseMocks,
debugOperations,
}),
},
},
});
it('renders', async () => {

const environment = createMockedGraphQLEnvironment();

const { findByTestId } = render(<TestFixture environment={environment} />);

expect(await findByTestId('user-properties')).toBeVisible();

});
return createGraphqlEnvironment({
. . .
interceptors: {
queryLevelMockerConfig: {
queryLevelMockerMiddleware: createQueryLevelMockerMiddleware({
schema,
mockEnums,
mockOverrides,
mockResolvers,
baseMocks,
debugOperations,
}),
},
},
});
const fieldLevelMockData = [
{
fieldOverride: [
{
path: 'viewer.userProperties.email',
value: 'mymockemail@test.com',
},
],
},
];

const environment = createGraphqlEnvironment({


. . .
interceptors: {
fieldLevelMockerConfig: {
mockData: fieldLevelMockData,
},
},
});
fieldOverride: [
{
path: 'viewer.userProperties.email',
value: null, // simulates a resolver error
},
{
path: 'updateUserAttribute', // mutation name
value: {
__typename: 'GenericError', // indicates union type
message: 'An error has occurred',
variables: {
// can be omitted to allow a match to any inputs
updateUserAttribute: {
input: {
value: 'John Doe',
attribute: 'name',
},
},
},
},
},
],
# clientSchema.graphql
extend type UserProperties {
city: String
}

const fieldLevelMockData = [
{
fieldOverride: [
{
path: 'viewer.userProperties.city',
value: 'Denver',
},
],
},
];
Development
Sandbox
Development
Sandbox
Benefits of Sophisticated Schema Mocking:
● Automatically mock queries Thank You!

○ Easy to write and maintain unit tests


○ Full code coverage by default
● Mocking individual fields
○ Reproduce incidents
○ Easy test specific scenarios
● Unlocks possibilities for other great tools
Improving User Experiences with
a Nullable Schema
Ernie Turner, Coinbase

#GraphQLConf

You might also like