I recently built an application at Deliveroo using Next.js / TypeScript and GraphQL (Apollo specifically). It was pretty cool. But it was lacking some decent end to end tests. My framework of choice was of course going to be Cypress.io but then I ran into some issues..
My client-side application was all setup to talk to my backend Ruby on Rails application via GraphQL. I started setting up Cypress in my client app and hooking it up in my CI config. But then I noticed that there wasn't a reliable way of stubbing out GraphQL requests to my remote GraphQL API ☹️ (or at least a solution didn't present itself to me at that exact moment in time).
So I settled on something half baked.. I decided to stub out any fetch
calls on the client-side so that any HTTP requests matching the path /graphql
were intercepted and then I created some JSON fixture files to return dependent on the GraphQL operationName
in the request body. It worked, but it was a bit shit.
Why was it shit you ask? Because:
fetch
just returned static JSON fixtures from the file system in the repository, which meant that any time I made tiny changes to the GraphQL schema on the Rails side, I would have to dig into a GraphQL response payload in development / production and copy the new payload structure into my fixtures.window.fetch
orientated mocking strategy to work, I had to disable SSR in my end to end testing environment, which in turn sometimes caused some unexpected environmental differences between the SSR-rendered pages and the client side rendered ones.I was aware that the graphql-tools
library provided the ability to introspect on a remote GraphQL schema and make it executable, but previous attempts at introspecting failed due to a variety of mundane reasons (auth issues, other projects taking precedence). This time I finally muddled through and got it working 👍
First I started creating a new mock-graphql-server
service in my client side application's docker-compose.yml
file:
version: '3'
services:
mock-graphql-server:
command: npm run start
ports:
- "3003:3003"
build:
context: ./mockserver
My mock server (which will be built in TypeScript) has a very simple Dockerfile
(your bog standard node one really):
FROM node:12-alpine
WORKDIR /app
COPY . /app
RUN npm install --ignore-scripts
RUN npm run build
CMD ["npm", "run", "start"]
The Dockerfile
above is fairly simple. All it does is builds my simple TypeScript / Node.js app, and defines the start
command. The corresponding package.json
commands are as follows:
{
"name": "mockserver",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"build": "tsc --project tsconfig.json",
"start": "node /app/index.js"
},
"author": "Adam Bull",
"license": "ISC",
"dependencies": {
"@graphql-tools/mock": "^6.0.10",
"@graphql-tools/schema": "^6.0.10",
"apollo-link-http": "^1.5.17",
"cross-fetch": "^3.0.5",
"express": "^4.17.1",
"express-graphql": "^0.9.0",
"graphql": "^15.1.0",
"graphql-tools": "^6.0.10",
"i": "^0.3.6",
"node-fetch": "^2.6.0",
"npm": "^6.14.5",
"typescript": "^3.9.5"
},
"devDependencies": {
"@deliveroo/tsconfig": "^1.0.0",
"@types/express": "^4.17.6",
"@types/express-graphql": "^0.9.0",
"@types/node-fetch": "^2.5.7"
}
}
Now, all that remains is to implement the mock GraphQL server.
npm start
just boots up a very simple Express server:
import graphqlHTTP from 'express-graphql';
import express from 'express';
import schema from './schema';
const app = express();
schema().then((sc) => {
app.use(
'/graphql',
graphqlHTTP({
schema: sc,
graphiql: true,
pretty: true,
}),
);
});
app.listen(3003, () => {
console.log('Running a GraphQL API server at http://localhost:3003/graphql');
});
I have opted to use the very lightweight express-graphql
server for the purposes of creating the mock gql server. I guess you could use Apollo Server too.
As you can see, all of the remote schema introspection + mocking magic happens in the schema
module...
import { fetch } from 'cross-fetch';
import { graphql, print } from 'graphql';
import {
introspectSchema,
wrapSchema,
AsyncExecutor,
addMocksToSchema,
IMocks,
} from 'graphql-tools';
interface ExecutorData {
document: any;
variables: any;
context: any;
}
const executor = async ({ document, variables, context }: ExecutorData) => {
const query = print(document);
const fetchResult = await fetch('http://host.docker.internal:3000/graphql', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer banana`,
},
body: JSON.stringify({ query, variables }),
});
return fetchResult.json();
};
export default async () => {
const remote = await introspectSchema(executor as AsyncExecutor);
return addMocksToSchema({ schema: remote, preserveResolvers: false });
};
In recent versions of graphql-tools
the API of introspectSchema
has changed to receive an executor
function which lets introspectSchema
know where to send its introspection queries.
Given that this is just an MVP, and that I didn't have the ability to auth with my production backend GraphQL API (I didn't have a bearer token to hand!), I just configured the introspection query to point at my backend application locally. This app was not defined in the same docker-compose
configuration as it is a separate repository, so I decided to make use of Docker's host.docker.internal
keyword to automatically interpolate the IP of the Docker host machine.
The express-graphql
library provides a graphiql
playground out of the box which allowed me to go and experiment with some GraphQL queries to see if mocked data was being returned.
I took some random GraphQL queries from my client app, and stuck them into the graphiql playground which is located at http://localhost:3003/graphql, and hey presto, mocked data is returned!
Interestingly, it looks like addMocksToSchema
automatically stubs out the values returned based on data type (you can see that I've misconfigured the types for some of my ids as strings, hence why "Hello world" is returned for some instances of originId
).