├── .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('
  • one
  • two
'); 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 ; 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(''); 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 | '
  • one
  • two
' 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('
  • one
  • two
'); 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 | '
  • one
  • two
' 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('
  • one
  • two
'); 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 | '
  • one
  • two
' 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 ; 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 ; 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 ; 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(''); 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(''); 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(''); 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 | --------------------------------------------------------------------------------