While working on a backend built using typescript, graphql, and mongoDB I reached a point where I needed some code coverage to continue development. Documenting test cases in graphql playground wasn't scaling anymore and I'm strictly against using Google Docs/Sheets for documenting test cases and running them manually after every code change. So, I decided to go with Jest for executing and documenting the test cases.
Overview
This article assumes that you have an existing backend codebase that uses typescript, graphql, and mongodb and is executed using docker. So, the basic infrastructure looks something like this, a docker-compose file runs the backend and database docker containers where the backend service can connect to the database container on a specific port. The backend service runs graphql and listens for connections on a specific port on the local machine and runs graphql playground for manually testing the queries & mutations.
Also, I set up my backend service to read the database connection details (url & name) from the environment variables, which comes from .env
file. Make sure to add this file to gitignore to ensure this doesn't get pushed to the remote and other environments (eg. production).
Setting up Jest
Assuming that you already have everything from the overview section running as expected and you're able to use graphql playground to execute queries and mututaions, we'll get started with setting up Jest in this section.
Since we're using Typescript, I will be working with ts-jest
.
Installation
Run the following commands in your terminal.
# Install pre-requisites
npm i -D jest typescript
# Install ts-jest
npm i -D ts-jest @types/jest
# Initiallize
npx ts-jest config:init
Once we've installed ts-jest
, let's add a test
script to our package.json
which will run jest on our codebase and execute all the *.test.js
tests files.
"scripts": {
/* ... */
"test": "jest"
},
Connecting to Database
In order to connect jest to our database, we would need to read the connection string from the environment variable. There are multiple ways to access environment variables in jest, so we'll cover the easiest method here.
We can tell jest when it gets executed to provide access to the environment variables inside test files by passing an argument to the jest call in package.json
.
"scripts": {
/* ... */
"test": "jest --setupFiles dotenv/config"
},
The --setupFiles dotenv/config
argument allows us to access environment variable within test files.
Now that we have access to the environment variables, let's write our first test which will connect to the database. src/first.test.ts
import mongoose, { Mongoose } from "mongoose";
let conn: Mongoose;
beforeAll(async () => {
conn = await mongoose.connect(
`${process.env.DATABASE_URL}/${process.env.DATABASE_NAME}`,
{
useNewUrlParser: true,
useUnifiedTopology: true,
useCreateIndex: true,
useFindAndModify: false,
}
);
});
afterAll(() => {
conn.disconnect();
});
Note that we don't have a test cases yet, but we're just setting up the test to connect to the database before starting and close the connection after completing all the tests.
Connecting to Graph for Testing
Now let's set up our test file to connect to our graph and call the queries and mutations from the schema. For this, we'll create a new graph that takes our schema and executes our queries & mutations on it.
Let's create a helper file that will set up the graph and help execute queries and mutations from test files. Let's call this file graphqlHelper.ts
:
import { graphql } from "graphql";
import { makeExecutableSchema } from "@graphql-tools/schema";
import { typeDefs } from "../typeDefs/typeDefs";
import { resolvers } from "../resolvers/resolvers";
import { User, IUser } from "../entity";
import { UserService } from "../services";
// Create the schema from typeDefs and resolvers
const schema = makeExecutableSchema({
typeDefs: typeDefs,
resolvers,
});
// Create the helper method that returns the graph
export const graphqlHelper = async (
query: any,
variables?: any,
user?: IUser,
) => {
// Create the graph
return graphql(
schema,
query, // Query or Mutations will be passed when calling this helper
undefined,
{
req: {
user, // add the user object to the req object
userId: user?._id, // add the userId object to the req object
},
res: {
cookie: () => {},
clearCookie: () => {}
},
// Add user & userId to context to simulate logged in state
user: user, // add the user object to the context
userId: user?._id, // add the userId object to the context
// Add the entities to the context
User,
// Add the services to the context
UserService,
},
variables // variables that gets passed when graphqlHelper method is called
);
};
Now that we have the graphqlHelper
method, we will import it in our test file first.test.js
import mongoose, { Mongoose } from "mongoose";
import { graphqlHelper } from "./graphqlHelper";
let conn: Mongoose;
beforeAll(async () => {
conn = await mongoose.connect(
`${process.env.DATABASE_URL}/${process.env.DATABASE_NAME}`,
{
useNewUrlParser: true,
useUnifiedTopology: true,
useCreateIndex: true,
useFindAndModify: false,
}
);
});
afterAll(() => {
conn.disconnect();
});
Writing tests
Now that we have everything set up, let's write some test cases in our first.test.js
file. For the purpose of this article I will be writing a simple register, login and me test case where we will test creating a new user using the register
mutation, logging in with that user using login
mutation and then fetching the logged in user's information using the me
query.
import mongoose, { Mongoose } from "mongoose";
import { graphqlHelper } from "./graphqlHelper";
import { User } from "../entity";
let conn: Mongoose;
beforeAll(async () => {
conn = await mongoose.connect(
`${process.env.DATABASE_URL}/${process.env.DATABASE_NAME}`,
{
useNewUrlParser: true,
useUnifiedTopology: true,
useCreateIndex: true,
useFindAndModify: false,
}
);
});
afterAll(async () => {
// Cleanup users created as part of test
await User.deleteMany({ email: /.*@example-jest-user-test.com/ });
conn.disconnect();
});
const registerMutation = `
mutation RegisterMutation($email: String!, $password: String!, $firstName: String!, $lastName: String!) {
register(email: $email, password: $password, firstName: $firstName, lastName: $lastName)
}
`;
const loginMutation = `
mutation LoginMutation($email: String!, $password: String!) {
login(email: $email, password: $password) {
_id
email
}
}
`;
const meQuery = `
query MeQuery {
me {
_id
email
}
}
`;
describe("resolvers", () => {
it("register, login, and me", async () => {
const randomNumber = Math.floor(1000 * Math.random());
const testUser = { email: `jestuser+${randomNumber}@example-jest-user-test.com`, password: "testpass", firstName: "Jest", lastName: "User" };
const registerResponse = await graphqlHelper(registerMutation, {
email: testUser.email,
password: testUser.password,
firstName: testUser.firstName,
lastName: testUser.lastName,
});
expect(registerResponse).toEqual({ data: { register: true } });
const dbUser = await User.findOne({ email: testUser.email });
expect(dbUser).toBeDefined();
const loginResponse = await graphqlHelper(loginMutation, {
email: testUser.email,
password: testUser.password,
});
expect(loginResponse).toEqual({
data: {
login: {
_id: `${dbUser!._id}`,
email: dbUser!.email,
}
}
});
const meResponse = await graphqlHelper(meQuery, {}, dbUser || undefined);
expect(meResponse).toEqual({
data: {
me: {
_id: `${dbUser!._id}`,
email: dbUser!.email,
}
}
});
});
});
Executing
We have the tests ready and now we just need to execute them. However, there's one problem. When you run npm run test
in your local (host) terminal, the test fails as the database connection times out. This is happening because the database is running inside docker and the container is only accessible from within the docker environment.
So, to execute, let's open the terminal of our backend docker container. To do this, we'll ssh into the docker container. We first need the ID of the backend container. So let's run this command in our terminal:
docker ps
And you should see an output like this:
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
8491cf18587f backend_server "docker-entrypoint.s…" 2 days ago Up 2 days 0.0.0.0:4000->4000/tcp, 0.0.0.0:9229->9229/tcp backend-server
0f15447c0ae2 mongo "docker-entrypoint.s…" 2 days ago Up 2 days 0.0.0.0:27017->27017/tcp db
From here, copy the CONTAINER ID of the backend server and then run the following command:
docker exec -it 8491cf18587f /bin/bash
where 8491cf18587f
is the CONTAINER ID of the backend docker container.
Once you have access to the container's terminal, navigate to the folder which contains the app files (including package.json
). This might look something like
cd /app/
depending on your docker configuration.
Once you're in that folder, run npm run test
to execute the tests within the docker container. This time you should see the tests get executed and an output that looks something like this
root@8491cf18587f:/app# npm run test
> server@0.0.1 test /app
> jest --setupFiles dotenv/config
PASS dist/first.test.js
PASS src/first.test.ts
Test Suites: 2 passed, 2 total
Tests: 2 passed, 2 total
Snapshots: 0 total
Time: 4.257 s
Ran all test suites.
root@8491cf18587f:/app#
Voila! You have succesfully executed your Jest tests from within your docker container. Now the only thing left to do it is to write a docker-compose file that will allow you to execute the tests from your local (host) terminal.
Docker-Compose
Let's write a docker-compose file that can execute our test cases from our terminal without having to connect to the container's terminal using SSH. Here is what we need to do to in this docker compose file, run the mongoDB image and then run the backend image, but in the backend image instead of running the command npm run dev
(or whatever is your execution script), we'll run npm run test
.
This is what the docker-compose.test.yml
file should look like:
version: "3.7"
services:
test-server:
build:
context: .
dockerfile: Dockerfile
target: base
volumes:
- ./src:/app/src
container_name: test-server
expose:
- 4000
ports:
- "4000:4000"
- "9229:9229"
command: npm run test
links:
- db
db:
container_name: db
image: mongo
ports:
- 27017:27017
volumes:
- data:/data/db
volumes:
data:
Now let's run this docker-compose file from the terminal:
docker-compose -f docker-compose.test.yml up
This will run the two containers and then execute the jest test scripts and print a similar output as before in the console. However, you'll see that running this will result in console showing the logs from the db container as well and if you want your console to look clean and only have the output of npm run test
then launch the docker compose using this command:
docker-compose -f docker-compose.test.yml run test-server
You should now see the output of the test case execution in your terminal.
Once you have executed the test cases, run the docker-compose down command to kill the containers.
docker-compose -f docker-compose.test.yml down
And that's it, you've successfully configured jest to execute test cases on your graphql typescript server inside docker!
Top comments (1)
[[..PingBack..]]
This article is curated as a part of #58th Issue of Software Testing Notes Newsletter.