├── .gitignore
├── src
├── config.ts
├── utils.ts
├── history.ts
├── expectations.ts
├── index.ts
├── mock.ts
└── client.ts
├── test
├── mocha.opts
├── history_test.ts
├── index_test.tsx
├── helper.tsx
├── expectations_test.ts
├── mock_test.ts
├── query_test.tsx
└── mutations_test.tsx
├── docs
├── index.md
├── api.md
├── setup.md
├── queries.md
└── mutations.md
├── .eslintrc
├── tsconfig.json
├── package.json
└── README.md
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | dist
3 |
--------------------------------------------------------------------------------
/src/config.ts:
--------------------------------------------------------------------------------
1 | export default class Config {
2 | allowUnmockedRequests = false;
3 | }
4 |
--------------------------------------------------------------------------------
/test/mocha.opts:
--------------------------------------------------------------------------------
1 | --require ts-node/register
2 | --reporter dot
3 | --recursive
4 | --timeout 3000
5 | test/**/*.{js,jsx,ts,tsx}
6 |
--------------------------------------------------------------------------------
/docs/index.md:
--------------------------------------------------------------------------------
1 | # GraphQLMock Docs
2 |
3 | * [Basic Setup](./setup.md)
4 | * [Testing Queries](./queries.md)
5 | * [Testing Mutations](./mutations.md)
6 | * [API Documentation](./api.md)
7 |
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "extends": [
3 | "compono-ui-ts"
4 | ],
5 | "rules": {
6 | "no-shadow": "off",
7 | "no-return-assign": "off",
8 | "prefer-destructuring": "off"
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "baseUrl": ".",
4 | "paths": {
5 | "*": ["*"]
6 | },
7 | "outDir": "./dist/",
8 | "target": "es6",
9 | "pretty": true,
10 | "allowSyntheticDefaultImports": true,
11 | "declaration": true,
12 | "module": "commonjs",
13 | "jsx": "react",
14 | "lib": [
15 | "dom",
16 | "es5",
17 | "es6",
18 | "es7",
19 | "esnext.asynciterable"
20 | ]
21 | },
22 | "include": [
23 | "./src/**/*"
24 | ]
25 | }
26 |
--------------------------------------------------------------------------------
/src/utils.ts:
--------------------------------------------------------------------------------
1 | import * as parser from 'graphql/language/parser';
2 | import * as printer from 'graphql/language/printer';
3 | import * as fastDeepEqual from 'fast-deep-equal';
4 | import { MockResponse } from './mock';
5 |
6 | export const { parse } = parser;
7 | export const stringify = printer.print;
8 | export const normalize = (query: string | object) => {
9 | const queryObject = typeof query === 'string' ? parse(query) : query;
10 | return stringify(queryObject as any);
11 | };
12 |
13 | export const isCompleted = (response: MockResponse): boolean =>
14 | response && !response.loading && response.networkStatus === 'ready';
15 |
16 | // TODO: make a more serious implementation of this
17 | export const fillIn = (query: string, variables: any = {}) => {
18 | let fullQuery = `${query}`;
19 |
20 | Object.keys(variables).forEach(key => {
21 | fullQuery = fullQuery.replace(`: $${key}`, `: ${JSON.stringify(variables[key])}`);
22 | });
23 |
24 | return fullQuery;
25 | };
26 |
27 | export const deepEqual = fastDeepEqual;
28 |
--------------------------------------------------------------------------------
/test/history_test.ts:
--------------------------------------------------------------------------------
1 | import History from '../src/history';
2 | import { parse } from '../src/utils';
3 | import { expect } from './helper';
4 |
5 | const query = parse(`
6 | query TodoDos {
7 | todos {
8 | id
9 | name
10 | }
11 | }
12 | `);
13 |
14 | const mutation = parse(`
15 | mutation CreateTodo($name: String!) {
16 | createToDo(name: $name) {
17 | id
18 | name
19 | }
20 | }
21 | `);
22 |
23 | describe('History', () => {
24 | let history;
25 | beforeEach(() => (history = new History()));
26 |
27 | it('initializes with an empty list of requests', () => {
28 | expect(history.requests).to.have.length(0);
29 | });
30 |
31 | it('allows to register new requests', () => {
32 | history.register({ query });
33 |
34 | expect(history.requests).to.eql([
35 | {
36 | query: 'query TodoDos {\n todos {\n id\n name\n }\n}\n',
37 | },
38 | ]);
39 | });
40 |
41 | it('allows to register mutations too', () => {
42 | history.register({ mutation, variables: { name: 'New entry' } });
43 |
44 | expect(history.requests).to.eql([
45 | {
46 | mutation: `mutation CreateTodo($name: String!) {\n createToDo(name: $name) {\n id\n name\n }\n}\n`,
47 | variables: { name: 'New entry' },
48 | },
49 | ]);
50 | });
51 | });
52 |
--------------------------------------------------------------------------------
/src/history.ts:
--------------------------------------------------------------------------------
1 | import { stringify, fillIn } from './utils';
2 |
3 | export interface Request {
4 | query?: string;
5 | mutation?: string;
6 | variables?: any;
7 | }
8 |
9 | export default class History {
10 | requests: Request[] = [];
11 |
12 | reset() {
13 | this.requests = [];
14 | }
15 |
16 | register({ query, mutation, variables }: any) {
17 | const request = {} as Request;
18 |
19 | if (query) {
20 | request.query = stringify(query);
21 | }
22 | if (mutation) {
23 | request.mutation = stringify(mutation);
24 | }
25 | if (variables) {
26 | request.variables = variables;
27 | }
28 |
29 | this.requests.push(request);
30 | }
31 |
32 | get queries() {
33 | return this.requests
34 | .filter(r => !!r.query)
35 | .map(({ query, variables }) => fillIn(query, variables));
36 | }
37 |
38 | get mutations() {
39 | return this.requests
40 | .filter(r => !!r.mutation)
41 | .map(({ mutation, variables }) => fillIn(mutation, variables));
42 | }
43 |
44 | get lastRequest() {
45 | return this.requests[this.requests.length - 1];
46 | }
47 |
48 | get lastQuery() {
49 | const { queries } = this;
50 | return queries[queries.length - 1];
51 | }
52 |
53 | get lastMutation() {
54 | const { mutations } = this;
55 | return mutations[mutations.length - 1];
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/test/index_test.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { ApolloClient } from 'apollo-client';
3 | import { mock, render, expect } from './helper';
4 | import { QueryComponent, query } from './query_test';
5 | import History from '../src/history';
6 |
7 | describe('GraphqlMock', () => {
8 | it('provides access to the mocked client', () => {
9 | expect(mock.client).to.be.instanceOf(ApolloClient);
10 | });
11 |
12 | it('provides access to the history object', () => {
13 | expect(mock.history).to.be.instanceOf(History);
14 | });
15 |
16 | it('resolves when query is mocked correctly', () => {
17 | mock.expect(query).reply({
18 | items: [{ id: '1', name: 'one' }, { id: '2', name: 'two' }],
19 | });
20 |
21 | const wrapper = render( );
22 |
23 | expect(wrapper.html()).to.eql('
');
24 | });
25 |
26 | it('explodes when query does not have an appropriate mock', () => {
27 | expect(() => {
28 | render( );
29 | }).to.throw(
30 | 'Unexpected GraphQL request:\nquery GetItems {\n items {\n id\n name\n }\n}\n'
31 | );
32 | });
33 |
34 | it('does not explode if one allows for requests to fall through', () => {
35 | mock.allowUnmockedRequests();
36 |
37 | const wrapper = render( );
38 |
39 | expect(wrapper.html()).to.contain('Loading...
');
40 |
41 | mock.allowUnmockedRequests(false);
42 | });
43 | });
44 |
--------------------------------------------------------------------------------
/src/expectations.ts:
--------------------------------------------------------------------------------
1 | import Mock from './mock';
2 | import { Request } from './history';
3 | import { normalize, deepEqual } from './utils';
4 |
5 | const mockMatch = (mock: Mock, request: Request) => {
6 | const queryMatch = request.query && mock.query === request.query;
7 | const mutationMatch = request.mutation && mock.query === request.mutation;
8 | const variablesMatch = mock.variables ? deepEqual(mock.variables, request.variables) : true;
9 |
10 | return (queryMatch || mutationMatch) && variablesMatch;
11 | };
12 |
13 | export default class Expectations {
14 | mocks: Mock[] = [];
15 |
16 | reset() {
17 | this.mocks = [];
18 | }
19 |
20 | expect(query: string | any) {
21 | let variables;
22 | let actualQuery;
23 |
24 | if (query.query || query.mutation) {
25 | // assuming it's an apollo-ish { query, variables } deal
26 | variables = query.variables;
27 | actualQuery = query.query || query.mutation;
28 | } else {
29 | // assuming it's a string or a parsed graphql query
30 | actualQuery = query;
31 | }
32 |
33 | const mock = new Mock({ query: normalize(actualQuery), variables });
34 | this.mocks.push(mock);
35 | return mock;
36 | }
37 |
38 | findMockResponseFor(request: Request) {
39 | const mock = this.mocks.find(m => mockMatch(m, request));
40 |
41 | if (!mock) {
42 | return null;
43 | }
44 |
45 | mock.register(request.variables);
46 |
47 | return mock.response;
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/test/helper.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { ApolloProvider } from 'react-apollo';
3 | import { ApolloProvider as BetaProvider } from '@apollo/react-common';
4 | import { ApolloProvider as HooksProvider } from 'react-apollo-hooks';
5 | import * as Enzyme from 'enzyme';
6 | import * as Adapter from 'enzyme-adapter-react-16';
7 | import { expect } from 'chai';
8 | import { JSDOM } from 'jsdom';
9 | import GraphQLMock from '../src';
10 |
11 | (global as any).expect = expect;
12 | export { expect };
13 |
14 | const { window } = new JSDOM('', {
15 | url: 'http://localhost/',
16 | });
17 |
18 | (global as any).window = window;
19 | (global as any).document = window.document;
20 | (global as any).navigator = window.navigator;
21 |
22 | // react 16 fake polyfill
23 | (global as any).requestAnimationFrame = callback => {
24 | setTimeout(callback, 0);
25 | };
26 |
27 | Enzyme.configure({ adapter: new Adapter() });
28 |
29 | const schema = `
30 | type Item {
31 | id: ID!
32 | name: String!
33 | }
34 |
35 | type Query {
36 | items: [Item]
37 | }
38 |
39 | type Mutation {
40 | createItem(name: String!): Item
41 | }
42 | `;
43 |
44 | export const mock = new GraphQLMock(schema);
45 |
46 | export const render = (element: JSX.Element) => {
47 | return Enzyme.mount(
48 |
49 |
50 | {element}
51 |
52 |
53 | );
54 | };
55 |
56 | beforeEach(() => {
57 | mock.reset();
58 | });
59 |
--------------------------------------------------------------------------------
/docs/api.md:
--------------------------------------------------------------------------------
1 | # API Documentation
2 |
3 | A `GraphqlMock` instance provides the following methods/properties
4 |
5 | ---
6 |
7 | `#expect(...).reply(...)` used for specifying regular responses and can be
8 | called in the folloing ways:
9 |
10 | 1. `mock.expect(query/mutation).reply(...)` - in this case the mock will trigger
11 | the reseponse for _any_ query / variables combination
12 | 2. `mock.expect({ query } or { mutation })` - the same as above
13 | 3. `mock.expect({ query, variables } or { mutation, variables })` - in this case
14 | the mock will trigger for this specific comibination of query and variables
15 | _only_!
16 |
17 | The query itself can be either a string or a `GraphQLQuery` object. You can
18 | either declare them anew in the tests (formatting doesn't matter) or re-use
19 | queries declared in your source code:
20 |
21 | Prior to `v2.0.0` query/mutation callbacks were not implemented and will not be
22 | fired when using `GraphqlMock` As of `v2.0.0` the `onCompleted` callback is
23 | implemented and will trigger automatically when using `GraphqlMock`. `onError`
24 | is yet to be implemented.
25 |
26 | ---
27 |
28 | `#expect(...).fail(...)` used to make specific queries/mutations to fail. the
29 | query/mutation/variables options are the same as above. The failure should be
30 | either a string, or an `ApolloError` instance.
31 |
32 | ---
33 |
34 | `#client` is a reference to the underline ApolloClient instance, which you
35 | should pass into `ApolloProvider` in your testing setup
36 |
37 | ---
38 |
39 | `#reset()` will reset all the mocks and queries history
40 |
41 | ---
42 |
43 | `#history` provides access to the history of all the queries and mutations (along
44 | with corresponding variables) that happened recently. In case you need to assert
45 | that specific queries did in fact happen
46 |
47 | ---
48 |
49 | `#allowUnmockedRequests(state)` -> switch on/off an option to allow unmocked queries to fall through
50 |
--------------------------------------------------------------------------------
/docs/setup.md:
--------------------------------------------------------------------------------
1 | # Setup & Configuration
2 |
3 | The setup is generally fairly straight forward
4 |
5 | ```
6 | npm add -D graphql-mock
7 | ```
8 |
9 | thow this somewhere in your testing environment setup
10 |
11 | ```js
12 | import GraphQLMock from 'graphql-mock';
13 |
14 | const typeDefs = `
15 | type Item {
16 | id: ID!
17 | name: String!
18 | }
19 |
20 | type Query {
21 | items: [Item]
22 | }
23 | `;
24 |
25 | const mock = new GraphQLMock(typeDefs);
26 | ```
27 |
28 | You can also pass optional mocks and resolvers if you need those to accompany your schema:
29 |
30 | ```js
31 | const mocks = {
32 | // the usual apollo mocks
33 | };
34 |
35 | const resolvers = {
36 | // the usual apollo resolvers
37 | };
38 |
39 | const mock = new GraphQLMock(typeDefs, mocks, resolvers);
40 | ```
41 |
42 | ## Re-using Existing Schema
43 |
44 | If you already have an executable schema defined somewhere in your project,
45 | you can pass it as is into the constructor as the first argument. This will
46 | work as well.
47 |
48 | ```js
49 | const mock = new GraphQLMock(myExecutableSchema);
50 | ```
51 |
52 | ## Tapping Into TestSuite
53 |
54 | It is generally a good idea to reset all the mocks, history and expectations
55 | before each test. You can accomplish this by calling the following:
56 |
57 | ```js
58 | beforeEach(() => mock.reset());
59 | ```
60 |
61 | ## Enabling/Disabling Unmocked Requests
62 |
63 | By default `GraphQLMock` will raise errors when it detects any unexpected
64 | queries or mutations. This is useful if one wants a watertight set of expectations
65 | from their code, what the code can and cannot call.
66 |
67 | But, if for some reason, you would prefer this would not happen, you can disable
68 | the feature by calling the following:
69 |
70 | ```js
71 | mock.allowUnmockedRequests(true);
72 | ```
73 |
74 | in this case all unrecognized requests will fall through and picked up by
75 | apollo schema link which will return the randomly generated schema from mocks and resolvers.
76 |
--------------------------------------------------------------------------------
/docs/queries.md:
--------------------------------------------------------------------------------
1 | # Testing Queries
2 |
3 | Lets assume you have a TodoList component that fires a graphql query to fetch the list
4 | from an API end point. Something like this:
5 |
6 | ```js
7 | import { Query } from 'react-apollo';
8 |
9 | const query = gql`
10 | query GetItems {
11 | items {
12 | id
13 | name
14 | }
15 | }
16 | `;
17 |
18 | const TodoList = () =>
19 |
20 | {({ data: { items = [] } = {}, error, loading }: any) =>
21 | if (loading) return Loading...
;
22 | if (error) return {error.message}
;
23 |
24 | return (
25 |
26 | {items.map(item =>
27 | {item.name}
28 | )}
29 |
30 | );
31 | }
32 | ;
33 | ```
34 |
35 | Here is how you can test it with enzyme and graphql-mock:
36 |
37 | ```js
38 | import { mount } from 'enzyme';
39 | import { ApolloProvider } from 'react-apollo';
40 | import graphqlMock from './graphql';
41 |
42 | const render = () => mount(
43 |
44 |
45 |
46 | );
47 |
48 | describe('TodoList', () => {
49 | it('renders todo items good', () => {
50 | graphqlMock.expect(query).reply({ // <- `query` from the code above
51 | items: [
52 | { id: '1', name: 'one' },
53 | { id: '2', name: 'two' }
54 | ]
55 | });
56 |
57 | expect(render().html()).toEqual('');
58 | });
59 |
60 | it('renders loading state too', () => {
61 | graphqlMock.expect(query).loading(true);
62 |
63 | expect(render().html()).toEqual('Loading...
');
64 | });
65 |
66 | it('renders errors when API fails', () => {
67 | graphqlMock.expect(query).fails('everything is terrible');
68 |
69 | expect(render().html()).toEqual('everything is terrible
');
70 | })
71 | });
72 | ```
73 |
74 | Please refer to the [API Documentation](./api.md) for more detailed information
75 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "graphql-mock",
3 | "version": "2.0.0",
4 | "description": "GraphQL endpoint mockery library for testing",
5 | "files": [
6 | "dist"
7 | ],
8 | "main": "dist/index.js",
9 | "types": "dist/index.d.ts",
10 | "scripts": {
11 | "build": "rm -rf dist && tsc",
12 | "lint": "eslint '{src,test}/**/*.{ts,tsx}'",
13 | "prepare": "npm run build",
14 | "test": "mocha"
15 | },
16 | "lint-staged": {
17 | "*.{ts,tsx}": [
18 | "eslint --fix",
19 | "git add"
20 | ]
21 | },
22 | "repository": {
23 | "type": "git",
24 | "url": "git+ssh://git@github.com/MadRabbit/graphql-mock.git"
25 | },
26 | "author": "Nikolay Nemshilov",
27 | "license": "MIT",
28 | "dependencies": {
29 | "apollo-link-schema": "^1.2.3",
30 | "fast-deep-equal": "^2.0.1",
31 | "graphql-tools": "^4.0.5"
32 | },
33 | "devDependencies": {
34 | "@apollo/react-common": "^3.1.4",
35 | "@apollo/react-hooks": "^3.1.2",
36 | "@types/chai": "^4.1.7",
37 | "@types/graphql": "^14.2.3",
38 | "@types/jsdom": "^12.2.4",
39 | "@types/mocha": "^5.2.7",
40 | "@types/nock": "^10.0.3",
41 | "@types/node": "^12.6.9",
42 | "@types/react": "^16.8.23",
43 | "apollo-cache-inmemory": "^1.6.2",
44 | "apollo-client": "^2.6.3",
45 | "babel-plugin-module-resolver": "^3.2.0",
46 | "chai": "^4.2.0",
47 | "enzyme": "^3.10.0",
48 | "enzyme-adapter-react-16": "^1.14.0",
49 | "eslint-config-compono-ui-ts": "^1.0.3",
50 | "graphql": "^14.4.2",
51 | "graphql-tag": "^2.10.1",
52 | "husky": "^3.0.1",
53 | "jsdom": "^15.1.1",
54 | "lint-staged": "^9.2.0",
55 | "mocha": "^6.2.0",
56 | "react": "^16.8.6",
57 | "react-apollo": "^2.5.8",
58 | "react-apollo-hooks": "^0.5.0",
59 | "react-dom": "^16.8.6",
60 | "sinon": "^10.0.0",
61 | "ts-node": "^8.3.0",
62 | "typescript": "^3.5.3"
63 | },
64 | "husky": {
65 | "hooks": {
66 | "pre-commit": "lint-staged",
67 | "pre-push": "npm test"
68 | }
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | import { GraphQLSchema } from 'graphql';
2 | import MockClient from './client';
3 | import Expectations from './expectations';
4 | import History from './history';
5 | import Mock from './mock';
6 | import Config from './config';
7 | import { isCompleted } from './utils';
8 |
9 | export * from './utils';
10 | export { Request } from './history';
11 | export { default as Mock } from './mock';
12 |
13 | export default class GraphQLMock {
14 | client: MockClient;
15 | config = new Config();
16 | history = new History();
17 | expectations = new Expectations();
18 |
19 | private args: any;
20 |
21 | constructor(schema: string | GraphQLSchema, mocks: object = {}, resolvers?: any) {
22 | this.args = { schema, mocks, resolvers };
23 | this.client = this.getNewClient();
24 | }
25 |
26 | getNewClient() {
27 | const { schema, mocks, resolvers } = this.args;
28 | const client = new MockClient(schema, mocks, resolvers);
29 |
30 | client.notify(({ query, mutation, variables, onCompleted }: any) => {
31 | this.history.register({ query, mutation, variables });
32 |
33 | const mockResponse = this.expectations.findMockResponseFor(this.history.lastRequest);
34 |
35 | if (mockResponse == null && !this.config.allowUnmockedRequests) {
36 | const request = this.history.lastRequest;
37 | const vars = request.variables
38 | ? `\nVARIABLES:\n${JSON.stringify(variables, null, 2)}\n`
39 | : '';
40 |
41 | throw new Error(`Unexpected GraphQL request:\n${request.query || request.mutation}${vars}`);
42 | }
43 |
44 | if (isCompleted(mockResponse) && onCompleted) onCompleted(mockResponse.data);
45 |
46 | return mockResponse;
47 | });
48 |
49 | return client;
50 | }
51 |
52 | reset() {
53 | this.history.reset();
54 | this.expectations.reset();
55 |
56 | this.client = this.getNewClient();
57 | }
58 |
59 | expect(query: string | any): Mock {
60 | return this.expectations.expect(query);
61 | }
62 |
63 | allowUnmockedRequests(state = true) {
64 | this.config.allowUnmockedRequests = state;
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/docs/mutations.md:
--------------------------------------------------------------------------------
1 | # Testing Mutations
2 |
3 | Lets pretend you have a componet that creates new TODO items:
4 |
5 | ```js
6 | import { Mutation } from 'react-apollo';
7 |
8 | const mutation = gql`
9 | mutation GetItems($name: String!) {
10 | createItem(name: $name) {
11 | id
12 | name
13 | }
14 | }
15 | `;
16 |
17 | const CreatorComponent = () =>
18 |
19 | {(createItem, { data, loading, error }: any) => {
20 | if (loading) return Loading...
;
21 | if (error) return {error.message}
;
22 | if (data) return {data.createItem.name}
;
23 |
24 | const onClick = () => createItem({ variables: { name: 'new item' } });
25 |
26 | return click me ;
27 | }}
28 | ;
29 | ```
30 |
31 | Now here how you can test this component through and through with graphql-mock:
32 |
33 | ```js
34 | const render = () => mount(
35 |
36 |
37 |
38 | );
39 |
40 | describe('CreatorComponent', () => {
41 | it('renders good by default', () => {
42 | expect(render().html()).toEqual('click me ');
43 | });
44 |
45 | it('sends mutations and renders responses', () => {
46 | graphqlMock.expect(mutation).reply({
47 | createItem: { id: 1, name: 'new item' }
48 | });
49 |
50 | const wrapper = render();
51 | wrapper.find('button').simulate('click');
52 |
53 | expect(wrapper.html()).toEqual('new item
');
54 | });
55 |
56 | it('sends correct variables with the request', () => {
57 | const mock = graphqlMock.expect(mutation).reply({
58 | createItem: { id: 1, name: 'new item' }
59 | });
60 |
61 | const wrapper = render();
62 | wrapper.find('button').simulate('click');
63 |
64 | expect(mock.calls[0]).toEqual([{ name: 'new item' }]);
65 | });
66 |
67 | it('can take a failure and live to fight another day', () => {
68 | graphqlMock.expect(mutation).fail('everything is terrible');
69 |
70 | const wrapper = render();
71 | wrapper.find('button').simulate('click');
72 |
73 | expect(wrapper.html()).toEqual('everything is terrible
');
74 | });
75 | });
76 | ```
77 |
78 | Please refer to the [API Documentation](./api.md) for more detailed information
79 |
--------------------------------------------------------------------------------
/src/mock.ts:
--------------------------------------------------------------------------------
1 | import { ApolloError } from 'apollo-client';
2 | import { deepEqual } from './utils';
3 |
4 | export interface Constructor {
5 | query: string;
6 | data?: any;
7 | error?: ApolloError;
8 | loading?: boolean;
9 | variables?: any;
10 | }
11 |
12 | export type MockResponse = {
13 | data: any;
14 | error?: ApolloError;
15 | loading: boolean;
16 | networkStatus: 'ready' | 'error';
17 | };
18 |
19 | export default class Mock {
20 | query: string;
21 | variables: any;
22 | calls: any[];
23 |
24 | private results = {
25 | data: undefined as any,
26 | error: undefined as ApolloError,
27 | loading: false as boolean,
28 | };
29 |
30 | constructor({ query, data = {}, error, loading = false, variables }: Constructor) {
31 | this.query = query;
32 | this.variables = variables;
33 | this.calls = [];
34 |
35 | this.reply(data);
36 | this.loading(loading);
37 |
38 | if (error) {
39 | this.fail(error);
40 | }
41 | }
42 |
43 | reply(data: any) {
44 | this.results.data = data;
45 | return this;
46 | }
47 |
48 | fail(error: any | any[] | string) {
49 | const errors = typeof error === 'string' ? [{ message: error }] : error;
50 |
51 | this.results.error =
52 | error instanceof ApolloError
53 | ? error
54 | : new ApolloError({
55 | graphQLErrors: Array.isArray(errors) ? errors : [errors],
56 | });
57 |
58 | return this;
59 | }
60 |
61 | loading(state: boolean = true) {
62 | this.results.loading = state;
63 | return this;
64 | }
65 |
66 | get response(): MockResponse {
67 | const { data, error, loading } = this.results;
68 | const response: any = { data, loading, networkStatus: error ? 'error' : 'ready' };
69 |
70 | if (error) {
71 | response.error = error;
72 | }
73 |
74 | return response;
75 | }
76 |
77 | register(variables: any) {
78 | this.calls.push([variables]);
79 | }
80 |
81 | // sinon mock interface
82 | get callCount() {
83 | return this.calls.length;
84 | }
85 |
86 | get notCalled() {
87 | return this.callCount === 0;
88 | }
89 |
90 | get called() {
91 | return this.callCount > 0;
92 | }
93 |
94 | get calledOnce() {
95 | return this.callCount === 1;
96 | }
97 |
98 | get calledTwice() {
99 | return this.callCount === 2;
100 | }
101 |
102 | calledWith(variables: any) {
103 | return this.calls.some(([vars]) => deepEqual(vars, variables));
104 | }
105 |
106 | calledOnceWith(variables: any) {
107 | return this.calls.filter(([vars]) => deepEqual(vars, variables)).length === 1;
108 | }
109 | }
110 |
--------------------------------------------------------------------------------
/src/client.ts:
--------------------------------------------------------------------------------
1 | import {
2 | ApolloClient,
3 | ApolloQueryResult,
4 | ObservableQuery,
5 | WatchQueryOptions,
6 | MutationOptions,
7 | OperationVariables,
8 | QueryOptions,
9 | } from 'apollo-client';
10 | import { InMemoryCache, NormalizedCacheObject } from 'apollo-cache-inmemory';
11 | import { SchemaLink } from 'apollo-link-schema';
12 | import { makeExecutableSchema, addMockFunctionsToSchema } from 'graphql-tools';
13 | import { GraphQLSchema } from 'graphql';
14 |
15 | export type AnyApolloOptions = WatchQueryOptions | MutationOptions;
16 |
17 | type Callback = (something: any) => void;
18 |
19 | const immediatelyResolvingPromise = (something: any) => {
20 | const promise = {
21 | then(callback: Callback) {
22 | return immediatelyResolvingPromise(callback(something));
23 | },
24 |
25 | catch() {
26 | return promise;
27 | },
28 |
29 | finally() {
30 | return promise;
31 | },
32 | };
33 |
34 | return promise;
35 | };
36 |
37 | const immediatelyFailingPromise = (error: Error) => {
38 | const promise = {
39 | then() {
40 | return promise;
41 | },
42 |
43 | catch(callback: Callback) {
44 | return immediatelyResolvingPromise(callback(error));
45 | },
46 |
47 | finally() {
48 | return promise;
49 | },
50 | };
51 |
52 | return promise;
53 | };
54 |
55 | const patchResponse = (original: any, mocked: any | void) => {
56 | if (mocked) {
57 | // mutations are handled as Promises in apollo
58 | if (original instanceof Promise) {
59 | if (mocked.error) {
60 | return immediatelyFailingPromise(mocked.error);
61 | }
62 | if (mocked.loading) {
63 | return Promise.resolve(mocked);
64 | }
65 |
66 | return immediatelyResolvingPromise(mocked);
67 | }
68 | // regular and subscription queries
69 | // NOTE `currentResult` is depricated in favour of `getCurrentResult`
70 | original.currentResult = original.getCurrentResult = () => mocked;
71 | }
72 |
73 | return original;
74 | };
75 |
76 | export default class MockClient extends ApolloClient {
77 | findMockFor: (options: AnyApolloOptions) => any | void;
78 |
79 | constructor(typeDefs: string | GraphQLSchema, mocks?: any, resolvers?: any) {
80 | const schema =
81 | typeof typeDefs === 'string' ? makeExecutableSchema({ typeDefs, resolvers }) : typeDefs;
82 |
83 | addMockFunctionsToSchema({ schema, mocks });
84 |
85 | const cache = new InMemoryCache((window as any).__APOLLO_STATE__); // eslint-disable-line
86 | const link = new SchemaLink({ schema });
87 |
88 | super({ link, cache, resolvers: {} });
89 | }
90 |
91 | notify(callback: (options: AnyApolloOptions) => any | void) {
92 | this.findMockFor = callback;
93 | }
94 |
95 | query(options: QueryOptions): Promise> {
96 | const result = super.query(options);
97 | return patchResponse(result, this.findMockFor(options));
98 | }
99 |
100 | watchQuery(
101 | options: WatchQueryOptions
102 | ): ObservableQuery {
103 | const result = super.watchQuery(options);
104 | return patchResponse(result, this.findMockFor(options));
105 | }
106 |
107 | mutate(options: MutationOptions): Promise {
108 | const result = super.mutate(options);
109 | return patchResponse(result, this.findMockFor(options));
110 | }
111 | }
112 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # GraphQL Client Side Mocking
2 |
3 | This is a library that helps with the apollo graphql projects testing.
4 | Essentially it provides for `nock`/`sinon` style declarative testing,
5 | and comparing to a vanilla apollo testing setup this enables the following:
6 |
7 | * specify an exact response data
8 | * test failure states
9 | * test loading states
10 | * assert which exact quieries and mutations sent by your code
11 | * assert exact variables that sent along with queries/mutations
12 |
13 | ## Quick Example
14 |
15 | ```js
16 | import { mount } from 'enzyme';
17 | import { ApolloProvider } from 'react-apollo';
18 | import { TodoList } from './components';
19 | import { graphqlMock } from './helper';
20 |
21 | const query = `
22 | query Todos {
23 | items {
24 | id
25 | name
26 | }
27 | }
28 | `;
29 |
30 | const render = () => mount(
31 |
32 |
33 |
34 | );
35 |
36 | describe(' ', () => {
37 | it('renders what server returns', () => {
38 | graphqlMock.expect(query).reply([
39 | items: [
40 | { id: '1', name: 'one' },
41 | { id: '2', name: 'two' }
42 | ]
43 | ]);
44 |
45 | expect(render().html()).toEqual(
46 | ''
47 | );
48 | });
49 |
50 | it('responds to failures gracefuly', () => {
51 | graphqlMock.expect(query).fail('everything is terrible');
52 | expect(render().html()).toEqual('everything is terrible
');
53 | });
54 |
55 | it('shows the loading state too', () => {
56 | graphqlMock.expect(query).loading(true);
57 | expect(render().html()).toEqual('Loading...
');
58 | });
59 | });
60 | ```
61 |
62 | Yes, it supports mutations too!
63 |
64 | ## Full Documentation
65 |
66 | * [Basic Setup](./docs/setup.md)
67 | * [Testing Queries](./docs/queries.md)
68 | * [Testing Mutations](./docs/mutations.md)
69 | * [API Documentation](./docs/api.md)
70 |
71 | ## react-apollo-hooks
72 |
73 | TL;DR maybe just use `@apollo/react-hooks`, it works pretty great? No need for any hackery
74 |
75 | `graphql-mock` will work with [react-apollo-hooks](https://github.com/trojanowski/react-apollo-hooks)
76 | as well. There are some caviates that relate to the internal implementation of react-apollo-hooks.
77 |
78 | Firstly it uses internal memoisation for the queries, so you will need a new client with every
79 | render/test. `mock.client` will now automatically return you a new client every time after
80 | `mock#reset()` called, so it should work fine, as long as you don't deconstruct the `client` into
81 | a variable outside of the render cycle.
82 |
83 | ```jsx
84 | // use this
85 |
86 | // ...
87 |
88 |
89 | // NOT THIS
90 | const { client } = graphqlMock;
91 |
92 | // ...
93 |
94 | ```
95 |
96 | Secondly `react-apollo-hooks` wrap mutation requests into an extra level of promises, which
97 | prevents us from processing the response right after an action. Meaning you'll need to wait
98 | and update the render wrapper:
99 |
100 | ```jsx
101 | graphqlMock.expect(mutation).reply({
102 | createItem: { id: 1, name: 'new item' }
103 | });
104 |
105 | const wrapper = render();
106 | wrapper.find('button').simulate('click');
107 |
108 | // you need to add those two
109 | await new Promise(r => setTimeout(r, 10));
110 | wrapper.update();
111 |
112 | expect(wrapper.html()).toEqual('new item
');
113 | ```
114 |
115 | ## Copyright & License
116 |
117 | All code in this library released under the terms of the MIT license
118 |
119 | Copyright (C) 2018-2019 Nikolay Nemshilov
120 |
--------------------------------------------------------------------------------
/test/expectations_test.ts:
--------------------------------------------------------------------------------
1 | import Mock from '../src/mock';
2 | import Expectations from '../src/expectations';
3 | import { normalize, parse } from '../src/utils';
4 | import { expect } from './helper';
5 |
6 | const query = `
7 | query Todos {
8 | todos {
9 | id
10 | name
11 | }
12 | }
13 | `;
14 |
15 | const mutation = `
16 | mutation CreateTodo($name: String!) {
17 | createTodo(name: $name) {
18 | id
19 | name
20 | }
21 | }
22 | `;
23 |
24 | describe('Expectations', () => {
25 | let expectations;
26 | beforeEach(() => (expectations = new Expectations()));
27 |
28 | it('starts with an empty list of mocks', () => {
29 | expect(expectations.mocks).to.eql([]);
30 | });
31 |
32 | describe('expect(query)', () => {
33 | it('allows to create a basic query expectation', () => {
34 | const mock = expectations.expect(query);
35 |
36 | expect(mock).to.be.instanceOf(Mock);
37 | expect(mock).to.eql(new Mock({ query: normalize(query) }));
38 | });
39 |
40 | it('allows to specify query as an object', () => {
41 | const mock = expectations.expect({ query });
42 | expect(mock).to.eql(new Mock({ query: normalize(query) }));
43 | });
44 |
45 | it('accepts a GraphQLQuery object', () => {
46 | const mock = expectations.expect(parse(query));
47 | expect(mock).to.eql(new Mock({ query: normalize(query) }));
48 | });
49 |
50 | it('accepts appolo query with a graphql query object', () => {
51 | const mock = expectations.expect({ query: parse(query) });
52 | expect(mock).to.eql(new Mock({ query: normalize(query) }));
53 | });
54 |
55 | it('accepts queries with variables', () => {
56 | const variables = { a: 1, b: 2 };
57 | const mock = expectations.expect({ query, variables });
58 | expect(mock).to.eql(new Mock({ query: normalize(query), variables }));
59 | });
60 |
61 | it('accepts mutations as well', () => {
62 | const mock = expectations.expect(mutation);
63 | expect(mock).to.eql(new Mock({ query: normalize(mutation) }));
64 | });
65 |
66 | it('accepts mutations + variables sets too', () => {
67 | const variables = { a: 1, b: 2 };
68 | const mock = expectations.expect({ mutation, variables });
69 | expect(mock).to.eql(new Mock({ query: normalize(mutation), variables }));
70 | });
71 | });
72 |
73 | describe('#findMockResponseFor', () => {
74 | const request = {
75 | query: normalize(query),
76 | variables: { a: 1 },
77 | };
78 |
79 | it('returns null if there is no mocks', () => {
80 | expect(expectations.findMockResponseFor(request)).to.equal(null);
81 | });
82 |
83 | it('returns null of no matching query is mocked', () => {
84 | expectations.expect(mutation);
85 | expect(expectations.findMockResponseFor(request)).to.equal(null);
86 | });
87 |
88 | it('finds a mock response if there is a matching query mock', () => {
89 | const mock = expectations.expect(query);
90 | expect(expectations.findMockResponseFor(request)).to.eql(mock.response);
91 | });
92 |
93 | it('returns the same response for any variables if only query was specified', () => {
94 | const mock = expectations.expect(query);
95 | const resp1 = expectations.findMockResponseFor({ ...request, variables: { a: 1 } });
96 | const resp2 = expectations.findMockResponseFor({ ...request, variables: { b: 2 } });
97 |
98 | expect(resp1).to.eql(mock.response);
99 | expect(resp2).to.eql(mock.response);
100 | });
101 |
102 | it('matches only specific pair if both query and variables were mocked', () => {
103 | const mock = expectations.expect({ query, variables: { b: 2 } });
104 |
105 | const resp1 = expectations.findMockResponseFor({ ...request, variables: { a: 1 } });
106 | const resp2 = expectations.findMockResponseFor({ ...request, variables: { b: 2 } });
107 |
108 | expect(resp1).to.equal(null);
109 | expect(resp2).to.eql(mock.response);
110 | });
111 |
112 | it('works with mutation mocks as well', () => {
113 | const mock = expectations.expect(mutation);
114 | const response = expectations.findMockResponseFor({
115 | mutation: normalize(mutation),
116 | variables: { b: 2 },
117 | });
118 |
119 | expect(response).to.eql(mock.response);
120 | });
121 | });
122 | });
123 |
--------------------------------------------------------------------------------
/test/mock_test.ts:
--------------------------------------------------------------------------------
1 | import { ApolloError } from 'apollo-client';
2 | import { GraphQLError } from 'graphql';
3 | import Mock from '../src/mock';
4 | import { expect } from './helper';
5 |
6 | const query = `
7 | some query
8 | `;
9 |
10 | describe('Mock', () => {
11 | let mock;
12 | beforeEach(() => (mock = new Mock({ query })));
13 |
14 | it('starts with an empty list of calls', () => {
15 | expect(mock.calls).to.have.length(0);
16 | });
17 |
18 | describe('#register(call)', () => {
19 | it('adds a call into the history', () => {
20 | mock.register({ a: 1 });
21 | expect(mock.calls).to.eql([[{ a: 1 }]]);
22 | });
23 | });
24 |
25 | describe('#reply(data)', () => {
26 | it('saves the data on the mock', () => {
27 | const data = { a: 1, b: 2 };
28 | mock.reply(data);
29 | expect(mock.results.data).to.eql(data);
30 | });
31 |
32 | it('returns the mock self reference back', () => {
33 | const result = mock.reply({ a: 1 });
34 | expect(result).to.eql(mock);
35 | });
36 | });
37 |
38 | describe('#fail(error)', () => {
39 | it('accepts regular strings as error messages', () => {
40 | mock.fail('everything is terrible');
41 | expect({ ...mock.results.error }).to.eql({
42 | ...new ApolloError({
43 | graphQLErrors: [new GraphQLError('everything is terrible')],
44 | }),
45 | });
46 | });
47 |
48 | it('accepts an array of objects as errors', () => {
49 | mock.fail([{ message: 'everything is terrible' }, { message: 'absolutely awful' }]);
50 | expect({ ...mock.results.error }).to.eql({
51 | ...new ApolloError({
52 | graphQLErrors: [
53 | new GraphQLError('everything is terrible'),
54 | new GraphQLError('absolutely awful'),
55 | ],
56 | }),
57 | });
58 | });
59 |
60 | it('accepts apollo errors as errors too', () => {
61 | const error = new ApolloError({
62 | graphQLErrors: [new GraphQLError('everything is terrible')],
63 | });
64 | mock.fail(error);
65 | expect(mock.results.error).to.eql(error);
66 | });
67 |
68 | it('returns a self reference', () => {
69 | const result = mock.fail('everything is terrible');
70 | expect(result).to.equal(mock);
71 | });
72 | });
73 |
74 | describe('#loading()', () => {
75 | it('switches the loading state to true by default', () => {
76 | mock.loading();
77 | expect(mock.results.loading).to.equal(true);
78 | });
79 |
80 | it('allows to switch loading state off', () => {
81 | mock.results.loading = true;
82 | mock.loading(false);
83 | expect(mock.results.loading).to.equal(false);
84 | });
85 |
86 | it('returns a self reference', () => {
87 | const result = mock.loading();
88 | expect(result).to.eql(mock);
89 | });
90 | });
91 |
92 | describe('#response', () => {
93 | it('returns correct data/loading/networkStatus for a regular mock', () => {
94 | const mock = new Mock({ query, data: { a: 1 } });
95 | expect(mock.response).to.eql({
96 | data: { a: 1 },
97 | loading: false,
98 | networkStatus: 'ready',
99 | });
100 | });
101 |
102 | it('returns correct data for an error as well', () => {
103 | const error = new ApolloError({
104 | graphQLErrors: [new GraphQLError('everything is terrible')],
105 | });
106 | const mock = new Mock({ query, error });
107 | expect(mock.response).to.eql({
108 | data: {},
109 | error,
110 | loading: false,
111 | networkStatus: 'error',
112 | });
113 | });
114 | });
115 |
116 | describe('sinon compatible interface', () => {
117 | it('provides access to callCount', () => {
118 | expect(mock.callCount).to.equal(0);
119 | mock.register({});
120 | expect(mock.callCount).to.equal(1);
121 | });
122 |
123 | it('checks whether the mock was not ever called', () => {
124 | expect(mock.notCalled).to.equal(true);
125 | mock.register({});
126 | expect(mock.notCalled).to.equal(false);
127 | });
128 |
129 | it('checks whether the mock was called', () => {
130 | expect(mock.called).to.equal(false);
131 | mock.register({});
132 | expect(mock.called).to.equal(true);
133 | });
134 |
135 | it('checks whether the mock was called just once', () => {
136 | expect(mock.calledOnce).to.equal(false);
137 | mock.register({});
138 | expect(mock.calledOnce).to.equal(true);
139 | mock.register({});
140 | expect(mock.calledOnce).to.equal(false);
141 | });
142 |
143 | it('checks if the mock was called with specific variables', () => {
144 | expect(mock.calledWith({ one: 1 })).to.equal(false);
145 | mock.register({ two: 2 });
146 | expect(mock.calledWith({ one: 1 })).to.equal(false);
147 | mock.register({ one: 1 });
148 | expect(mock.calledWith({ one: 1 })).to.equal(true);
149 | });
150 |
151 | it('checks if the mock was called only once with the variables', () => {
152 | expect(mock.calledOnceWith({ one: 1 })).to.equal(false);
153 | mock.register({ two: 2 });
154 | expect(mock.calledOnceWith({ one: 1 })).to.equal(false);
155 | mock.register({ one: 1 });
156 | expect(mock.calledOnceWith({ one: 1 })).to.equal(true);
157 | mock.register({ one: 1 });
158 | expect(mock.calledOnceWith({ one: 1 })).to.equal(false);
159 | });
160 | });
161 | });
162 |
--------------------------------------------------------------------------------
/test/query_test.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { Query } from 'react-apollo';
3 | import * as sinon from 'sinon';
4 | import { useQuery } from '@apollo/react-hooks';
5 | import { useQuery as rahUseQuery } from 'react-apollo-hooks';
6 | import gql from 'graphql-tag';
7 | import { mock, render, expect } from './helper';
8 | import { normalize } from '../src/utils';
9 |
10 | const sandbox = sinon.createSandbox();
11 |
12 | export const query = gql`
13 | query GetItems {
14 | items {
15 | id
16 | name
17 | }
18 | }
19 | `;
20 |
21 | const ToDos = ({ items, error, loading }: any) => {
22 | if (loading) {
23 | return Loading...
;
24 | }
25 | if (error) {
26 | return {error.message}
;
27 | }
28 |
29 | return (
30 |
31 | {items.map(item => (
32 | {item.name}
33 | ))}
34 |
35 | );
36 | };
37 |
38 | export const QueryComponent = () => (
39 |
40 | {({ data: { items = [] } = {}, error, loading }: any) => (
41 |
42 | )}
43 |
44 | );
45 |
46 | export const ApolloHooksQueryComponent = () => {
47 | const { data, error, loading } = useQuery(query);
48 | const { items = [] } = data || {};
49 |
50 | return ;
51 | };
52 |
53 | export const HookedQueryComponent = () => {
54 | const { data, error, loading } = rahUseQuery(query);
55 | const { items = [] } = data || {};
56 |
57 | return ;
58 | };
59 |
60 | export const CompleteCallbackComponent = ({ onCompleted }: any) => {
61 | const { data, error, loading } = useQuery(query, { onCompleted });
62 | const { items = [] } = data || {};
63 |
64 | return ;
65 | };
66 |
67 | describe('query mocking', () => {
68 | afterEach(() => sandbox.restore());
69 |
70 | describe('Wrapped component', () => {
71 | it('handles mocked response', () => {
72 | mock.expect(query).reply({
73 | items: [{ id: '1', name: 'one' }, { id: '2', name: 'two' }],
74 | });
75 |
76 | expect(render( ).html()).to.eql('');
77 | });
78 |
79 | it('allows to mock error states too', () => {
80 | mock.expect(query).fail('everything is terrible');
81 |
82 | expect(render( ).html()).to.eql(
83 | 'GraphQL error: everything is terrible
'
84 | );
85 | });
86 |
87 | it('allows to simulate loading state too', () => {
88 | mock.expect(query).loading(true);
89 | expect(render( ).html()).to.eql('Loading...
');
90 | });
91 | });
92 |
93 | describe('apollo-hooks component', () => {
94 | it('handles mocked response', () => {
95 | mock.expect(query).reply({
96 | items: [{ id: '1', name: 'one' }, { id: '2', name: 'two' }],
97 | });
98 |
99 | expect(render( ).html()).to.eql(
100 | ''
101 | );
102 | });
103 |
104 | it('allows to mock error states too', () => {
105 | mock.expect(query).fail('everything is terrible');
106 |
107 | expect(render( ).html()).to.eql(
108 | 'GraphQL error: everything is terrible
'
109 | );
110 | });
111 |
112 | it('allows to simulate loading state too', () => {
113 | mock.expect(query).loading(true);
114 | expect(render( ).html()).to.eql('Loading...
');
115 | });
116 | });
117 |
118 | describe('hooked component', () => {
119 | it('handles mocked response', () => {
120 | mock.expect(query).reply({
121 | items: [{ id: '1', name: 'one' }, { id: '2', name: 'two' }],
122 | });
123 |
124 | expect(render( ).html()).to.eql('');
125 | });
126 |
127 | it('allows to mock error states too', () => {
128 | mock.expect(query).fail('everything is terrible');
129 |
130 | expect(render( ).html()).to.eql(
131 | 'GraphQL error: everything is terrible
'
132 | );
133 | });
134 |
135 | it('allows to simulate loading state too', () => {
136 | mock.expect(query).loading(true);
137 | expect(render( ).html()).to.eql('Loading...
');
138 | });
139 | });
140 |
141 | describe('onCompleted callback', () => {
142 | it('triggers on success', () => {
143 | const response = { items: [{ id: '1', name: 'one' }, { id: '2', name: 'two' }] };
144 | mock.expect(query).reply(response);
145 |
146 | const spy = sandbox.spy();
147 |
148 | expect(render( ).html()).to.eql(
149 | ''
150 | );
151 |
152 | expect(spy.firstCall.args[0]).to.eql(response);
153 | });
154 |
155 | it('does not trigger on failure', () => {
156 | mock.expect(query).fail('everything is terrible');
157 |
158 | const spy = sandbox.spy();
159 |
160 | expect(render( ).html()).to.eql(
161 | 'GraphQL error: everything is terrible
'
162 | );
163 |
164 | expect(spy.callCount).to.equal(0);
165 | });
166 |
167 | it('does not trigger cwhen loading', () => {
168 | mock.expect(query).loading(true);
169 |
170 | const spy = sandbox.spy();
171 |
172 | expect(render( ).html()).to.eql(
173 | 'Loading...
'
174 | );
175 |
176 | expect(spy.callCount).to.equal(0);
177 | });
178 | });
179 |
180 | it('registers requests in the history', () => {
181 | mock.expect(query).reply({ items: [] });
182 | render( );
183 | expect(mock.history.requests).to.eql([
184 | {
185 | query: normalize(query),
186 | variables: {},
187 | },
188 | ]);
189 | });
190 | });
191 |
--------------------------------------------------------------------------------
/test/mutations_test.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { Mutation } from 'react-apollo';
3 | import { useMutation } from '@apollo/react-hooks';
4 | import { useMutation as rahUseMutation } from 'react-apollo-hooks';
5 | import gql from 'graphql-tag';
6 | import { mock, render, expect } from './helper';
7 | import { normalize } from '../src/utils';
8 |
9 | const mutation = gql`
10 | mutation CreateItem($name: String!) {
11 | createItem(name: $name) {
12 | id
13 | name
14 | }
15 | }
16 | `;
17 |
18 | const noop = () => null; // silence errors
19 |
20 | const MutatorComponent = () => (
21 |
22 | {(createItem, { data, loading, error }: any) => {
23 | if (loading) {
24 | return Loading...
;
25 | }
26 | if (error) {
27 | return {error.message}
;
28 | }
29 | if (data) {
30 | return {data.createItem.name}
;
31 | }
32 |
33 | const onClick = () => createItem({ variables: { name: 'new item' } });
34 |
35 | return click me ;
36 | }}
37 |
38 | );
39 |
40 | const ApolloHooksComponent = () => {
41 | const [createItem, { error, data, loading }] = useMutation(mutation, { onError: noop });
42 | const onClick = () => createItem({ variables: { name: 'new item' } });
43 |
44 | if (loading) {
45 | return Loading...
;
46 | }
47 | if (error) {
48 | return {error.message}
;
49 | }
50 | if (data) {
51 | return {data.createItem.name}
;
52 | }
53 |
54 | return click me ;
55 | };
56 |
57 | const HookedMutatorComponent = () => {
58 | const [response, setResponse] = React.useState({ data: null, loading: false, error: null });
59 | const [createItem] = rahUseMutation(mutation);
60 | const onClick = () =>
61 | createItem({ variables: { name: 'new item' } })
62 | .then(result => setResponse(result as any))
63 | .catch(error => setResponse({ data: null, loading: false, error }));
64 |
65 | const { data, loading, error } = response;
66 |
67 | if (loading) {
68 | return Loading...
;
69 | }
70 | if (error) {
71 | return {error.message}
;
72 | }
73 | if (data) {
74 | return {data.createItem.name}
;
75 | }
76 |
77 | return click me ;
78 | };
79 |
80 | describe('mutations', () => {
81 | describe('wrapped mutation component', () => {
82 | it('allows to mock mutation queries', () => {
83 | mock.expect(mutation).reply({
84 | createItem: { id: 1, name: 'new item' },
85 | });
86 |
87 | const wrapper = render( );
88 | expect(wrapper.html()).to.eql('click me ');
89 |
90 | wrapper.find('button').simulate('click');
91 | expect(wrapper.html()).to.eql('new item
');
92 | });
93 |
94 | it('allows to specify a failure response', () => {
95 | mock.expect(mutation).fail('everything is terrible');
96 |
97 | const wrapper = render( );
98 | wrapper.find('button').simulate('click');
99 |
100 | expect(wrapper.html()).to.eql('GraphQL error: everything is terrible
');
101 | });
102 |
103 | it('allows to test the mutation loading state', () => {
104 | mock
105 | .expect(mutation)
106 | .loading(true)
107 | .reply({ createItem: {} });
108 |
109 | const wrapper = render( );
110 | wrapper.find('button').simulate('click');
111 |
112 | expect(wrapper.html()).to.eql('Loading...
');
113 | });
114 |
115 | it('registers mutation calls in the history', () => {
116 | mock.expect(mutation).reply({
117 | createItem: { id: 1, name: 'new item' },
118 | });
119 |
120 | const wrapper = render( );
121 | wrapper.find('button').simulate('click');
122 |
123 | expect(mock.history.requests).to.eql([
124 | {
125 | mutation: normalize(mutation),
126 | variables: { name: 'new item' },
127 | },
128 | ]);
129 | });
130 |
131 | it('allows to verify the exact variables that the mutation has been called with', () => {
132 | const mut = mock.expect(mutation).reply({
133 | createItem: { id: 1, name: 'new item' },
134 | });
135 |
136 | const wrapper = render( );
137 | wrapper.find('button').simulate('click');
138 |
139 | expect(mut.calls).to.eql([[{ name: 'new item' }]]);
140 | });
141 | });
142 |
143 | describe('wrapped mutation component', () => {
144 | it('allows to mock mutation queries', () => {
145 | mock.expect(mutation).reply({
146 | createItem: { id: 1, name: 'new item' },
147 | });
148 |
149 | const wrapper = render( );
150 | expect(wrapper.html()).to.eql('click me ');
151 |
152 | wrapper.find('button').simulate('click');
153 | expect(wrapper.html()).to.eql('new item
');
154 | });
155 |
156 | it('allows to specify a failure response', () => {
157 | mock.expect(mutation).fail('everything is terrible');
158 |
159 | const wrapper = render( );
160 | wrapper.find('button').simulate('click');
161 |
162 | expect(wrapper.html()).to.eql('GraphQL error: everything is terrible
');
163 | });
164 |
165 | it('allows to test the mutation loading state', () => {
166 | mock
167 | .expect(mutation)
168 | .loading(true)
169 | .reply({ createItem: {} });
170 |
171 | const wrapper = render( );
172 | wrapper.find('button').simulate('click');
173 |
174 | expect(wrapper.html()).to.eql('Loading...
');
175 | });
176 |
177 | it('registers mutation calls in the history', () => {
178 | mock.expect(mutation).reply({
179 | createItem: { id: 1, name: 'new item' },
180 | });
181 |
182 | const wrapper = render( );
183 | wrapper.find('button').simulate('click');
184 |
185 | expect(mock.history.requests).to.eql([
186 | {
187 | mutation: normalize(mutation),
188 | variables: { name: 'new item' },
189 | },
190 | ]);
191 | });
192 |
193 | it('allows to verify the exact variables that the mutation has been called with', () => {
194 | const mut = mock.expect(mutation).reply({
195 | createItem: { id: 1, name: 'new item' },
196 | });
197 |
198 | const wrapper = render( );
199 | wrapper.find('button').simulate('click');
200 |
201 | expect(mut.calls).to.eql([[{ name: 'new item' }]]);
202 | });
203 | });
204 |
205 | describe('Hooked mutation component', () => {
206 | const sleep = () => new Promise(r => setTimeout(r, 10));
207 |
208 | it('allows to mock mutation queries', async () => {
209 | mock.expect(mutation).reply({
210 | createItem: { id: 1, name: 'new item' },
211 | });
212 |
213 | const wrapper = render( );
214 | expect(wrapper.html()).to.eql('click me ');
215 |
216 | wrapper.find('button').simulate('click');
217 | await sleep();
218 | wrapper.update();
219 |
220 | expect(wrapper.html()).to.eql('new item
');
221 | });
222 |
223 | it('allows to specify a failure response', async () => {
224 | mock.expect(mutation).fail('everything is terrible');
225 |
226 | const wrapper = render( );
227 | wrapper.find('button').simulate('click');
228 | await sleep();
229 | wrapper.update();
230 |
231 | expect(wrapper.html()).to.eql('GraphQL error: everything is terrible
');
232 | });
233 |
234 | it('allows to test the mutation loading state', async () => {
235 | mock
236 | .expect(mutation)
237 | .loading(true)
238 | .reply({ createItem: {} });
239 |
240 | const wrapper = render( );
241 | wrapper.find('button').simulate('click');
242 | await sleep();
243 | wrapper.update();
244 |
245 | expect(wrapper.html()).to.eql('Loading...
');
246 | });
247 |
248 | it('registers mutation calls in the history', async () => {
249 | mock.expect(mutation).reply({
250 | createItem: { id: 1, name: 'new item' },
251 | });
252 |
253 | const wrapper = render( );
254 | wrapper.find('button').simulate('click');
255 | await sleep();
256 | wrapper.update();
257 |
258 | expect(mock.history.requests).to.eql([
259 | {
260 | mutation: normalize(mutation),
261 | variables: { name: 'new item' },
262 | },
263 | ]);
264 | });
265 |
266 | it('allows to verify the exact variables that the mutation has been called with', async () => {
267 | const mut = mock.expect(mutation).reply({
268 | createItem: { id: 1, name: 'new item' },
269 | });
270 |
271 | const wrapper = render( );
272 | wrapper.find('button').simulate('click');
273 | await sleep();
274 | wrapper.update();
275 |
276 | expect(mut.calls).to.eql([[{ name: 'new item' }]]);
277 | });
278 | });
279 | });
280 |
--------------------------------------------------------------------------------