Skip to content
This repository has been archived by the owner on Oct 1, 2024. It is now read-only.

Commit

Permalink
[WIP] Modernize graphql testing (#623)
Browse files Browse the repository at this point in the history
  • Loading branch information
lemonmade authored Mar 31, 2019
1 parent abcf2b1 commit 949ea28
Show file tree
Hide file tree
Showing 17 changed files with 256 additions and 341 deletions.
9 changes: 7 additions & 2 deletions packages/graphql-testing/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@ and adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).

## [Unreleased]

### Added
### Changed

- `@shopify/graphql-testing` package - next generation of `jest-mock-apollo` with rename of the package
- `createGraphQLFactory` is now a named export, not the default export ([#623](https://github.com/Shopify/quilt/pull/623/))
- Simplified much of the external workings of the library, including removing the custom subclass of `ApolloClient` ([#623](https://github.com/Shopify/quilt/pull/623/))

## [1.0.0]

Initial release.
4 changes: 2 additions & 2 deletions packages/graphql-testing/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ Note: In a typical application you will want to generalized some of this impleme
```ts
import {mount} from 'enzyme';
import {ApolloProvider} from 'react-apollo';
import createGraphQLFactory from '@shopify/graphql-testing';
import {createGraphQLFactory} from '@shopify/graphql-testing';

export const createGraphQL = createGraphQLFactory();

Expand Down Expand Up @@ -94,7 +94,7 @@ Below is an example of how to assert that a graphQL request was triggered.
import {mount} from 'enzyme';
import {ApolloProvider} from 'react-apollo';
import {trigger} from '@shopify/enzyme-utilities';
import createGraphQLFactory from '@shopify/graphql-testing';
import {createGraphQLFactory} from '@shopify/graphql-testing';

export const createGraphQL = createGraphQLFactory();

Expand Down
45 changes: 0 additions & 45 deletions packages/graphql-testing/src/MemoryApolloClient.ts

This file was deleted.

85 changes: 53 additions & 32 deletions packages/graphql-testing/src/createGraphQLFactory.ts
Original file line number Diff line number Diff line change
@@ -1,37 +1,33 @@
import {GraphQLRequest} from 'apollo-link';
import {GraphQLRequest, ApolloLink} from 'apollo-link';
import {
ApolloReducerConfig,
InMemoryCache,
IntrospectionFragmentMatcher,
} from 'apollo-cache-inmemory';
import {ApolloClientOptions} from 'apollo-client';
import {ApolloClient} from 'apollo-client';
import {EventEmitter} from 'events';

import MemoryApolloClient from './MemoryApolloClient';
import MockApolloLink from './MockApolloLink';
import Operations from './Operations';
import {GraphQLMock} from './types';
import {MockLink, InflightLink} from './links';
import {Operations} from './utilities';
import {GraphQLMock, MockRequest} from './types';

export interface Options {
unionOrIntersectionTypes?: any[];
cacheOptions?: ApolloReducerConfig;
}

function defaultGraphQLMock({operationName}: GraphQLRequest) {
return new Error(
`Can’t perform GraphQL operation '${operationName ||
''}' because no mocks were set.`,
);
}
export class GraphQL extends EventEmitter {
readonly client: ApolloClient<unknown>;
readonly operations = new Operations();

class GraphQL {
client: MemoryApolloClient;
readonly operations: Operations;
private afterResolver: (() => void) | undefined;
private pendingRequests = new Set<MockRequest>();

constructor(
mock: GraphQLMock,
{unionOrIntersectionTypes = [], cacheOptions = {}}: Options = {},
) {
super();

const cache = new InMemoryCache({
fragmentMatcher: new IntrospectionFragmentMatcher({
introspectionQueryResultData: {
Expand All @@ -43,32 +39,57 @@ class GraphQL {
...cacheOptions,
});

const mockApolloClientOptions: ApolloClientOptions<unknown> = {
link: new MockApolloLink(mock),
cache,
};

const memoryApolloClient = new MemoryApolloClient(mockApolloClientOptions);
const link = ApolloLink.from([
new InflightLink({
onCreated: this.handleCreate,
onResolved: this.handleResolve,
}),
new MockLink(mock),
]);

this.client = memoryApolloClient;
this.operations = memoryApolloClient.operations;
this.client = new ApolloClient({
link,
cache,
});
}

afterResolve(resolver: () => void) {
this.afterResolver = resolver;
on(event: 'pre-resolve', handler: () => void): this;
on(event: 'post-resolve', handler: () => void): this;
on(event: string, handler: (...args: any[]) => void) {
return super.on(event, handler);
}

async resolveAll() {
await this.client.resolveAll();
const promise = Promise.all(
Array.from(this.pendingRequests).map(({resolve}) => resolve()),
);

this.emit('pre-resolve');

if (this.afterResolver) {
this.afterResolver();
}
await promise;

this.emit('post-resolve');
}

private handleCreate = (request: MockRequest) => {
this.pendingRequests.add(request);
};

private handleResolve = (request: MockRequest) => {
this.operations.push(request.operation);
this.pendingRequests.delete(request);
};
}

function defaultGraphQLMock({operationName}: GraphQLRequest) {
return new Error(
`Can’t perform GraphQL operation '${operationName ||
''}' because no mocks were set.`,
);
}

export default function createGraphQLFactory(options?: Options) {
return function createGraphQLClient(mock: GraphQLMock = defaultGraphQLMock) {
export function createGraphQLFactory(options?: Options) {
return function createGraphQL(mock: GraphQLMock = defaultGraphQLMock) {
return new GraphQL(mock, options);
};
}
3 changes: 1 addition & 2 deletions packages/graphql-testing/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,2 @@
export {default} from './createGraphQLFactory';

export * from './createGraphQLFactory';
export * from './types';
2 changes: 2 additions & 0 deletions packages/graphql-testing/src/links/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export {InflightLink} from './inflight';
export {MockLink} from './mocks';
Original file line number Diff line number Diff line change
@@ -1,16 +1,14 @@
import {ApolloLink, Observable, Operation, NextLink} from 'apollo-link';

import {MockRequest} from './types';
import {MockRequest} from '../types';

export default class MemoryApolloLink extends ApolloLink {
private onRequestCreatedCallback:
| ((request: MockRequest) => void)
| undefined;
private onRequestResolvedCallback:
| ((request: MockRequest) => void)
| undefined;
interface Options {
onCreated(request: MockRequest): void;
onResolved(request: MockRequest): void;
}

constructor() {
export class InflightLink extends ApolloLink {
constructor(private options: Options) {
super();
}

Expand All @@ -29,14 +27,12 @@ export default class MemoryApolloLink extends ApolloLink {
operation,
resolve: () => {
resolver();
this.onRequestResolvedCallback &&
this.onRequestResolvedCallback(request);

this.options.onResolved(request);
return promise;
},
};

this.onRequestCreatedCallback && this.onRequestCreatedCallback(request);
this.options.onCreated(request);

return new Observable(observer => {
return nextLink(operation).subscribe({
Expand All @@ -55,12 +51,4 @@ export default class MemoryApolloLink extends ApolloLink {
});
});
}

onRequestCreated(onRequestCreated: (request: MockRequest) => void) {
this.onRequestCreatedCallback = onRequestCreated;
}

onRequestResolved(onRequestResolved: (request: MockRequest) => void) {
this.onRequestResolvedCallback = onRequestResolved;
}
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import {ApolloLink, Observable, Operation} from 'apollo-link';
import {ExecutionResult, GraphQLError} from 'graphql';
import {GraphQLMock, MockGraphQLFunction} from './types';
import {GraphQLMock, MockGraphQLFunction} from '../types';

export default class MockApolloLink extends ApolloLink {
export class MockLink extends ApolloLink {
constructor(private mock: GraphQLMock) {
super();
}
Expand Down
73 changes: 73 additions & 0 deletions packages/graphql-testing/src/links/tests/mocks.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import gql from 'graphql-tag';

import {MockGraphQLResponse} from '../../types';
import {MockLink} from '../mocks';

import {executeOnce} from './utilities';

const petQuery = gql`
query Pet {
pets {
...CatInfo
}
}
fragment CatInfo on Cat {
name
}
`;

describe('MockLink', () => {
it('returns a result when there is an object matching an operation', async () => {
const data = {pets: [{name: 'Spike'}]};
const link = new MockLink({Pet: data});
const {result} = await executeOnce(link, petQuery);
expect(result).toEqual({data});
});

it('returns a result when there is an function matching an operation', async () => {
const data = {pets: [{name: 'Spike'}]};
const spy = jest.fn(() => data);
const link = new MockLink({Pet: spy});
const {result} = await executeOnce(link, petQuery);

expect(result).toEqual({data});
expect(spy).toHaveBeenCalledWith(
expect.objectContaining({query: petQuery}),
);
});

it('returns an error message when the mock looks like a fixture', async () => {
const link = new MockLink({});
const {result} = await executeOnce(link, petQuery);

expect(result).toMatchObject(
new Error(
"Can’t perform GraphQL operation 'Pet' because no valid mocks were found (it looks like you tried to provide data directly to the mock GraphQL client. You need to provide your fixture on the key that matches its operation name. To fix this, simply change your code to read 'mockGraphQLClient({Pet: yourFixture}).'",
),
);
});

it('returns an error message when there are no matching mocks', async () => {
const link = new MockLink({LostPets: {}, PetsForSale: {}});
const {result} = await executeOnce(link, petQuery);

expect(result).toMatchObject(
new Error(
"Can’t perform GraphQL operation 'Pet' because no valid mocks were found (you provided an object that had mocks only for the following operations: LostPets, PetsForSale).",
),
);
});

it('returns an error message when no fixture is returned from a mock', async () => {
const link = new MockLink(() => (null as unknown) as MockGraphQLResponse);

const {result} = await executeOnce(link, petQuery);

expect(result).toMatchObject(
new Error(
"Can’t perform GraphQL operation 'Pet' because no valid mocks were found (you provided a function that did not return a valid mock result)",
),
);
});
});
36 changes: 0 additions & 36 deletions packages/graphql-testing/src/test/MemoryApolloClient.test.ts

This file was deleted.

Loading

0 comments on commit 949ea28

Please sign in to comment.