In the lasts years, GraphQL becomes in one of the preferred alternatives to REST architectures, and its popularity is growing up day to day due to its many advantages for the developers request just the data they need.
Testing is the process to evaluate the functionality of a software application with an intent to find issues or defects in our software or according to ANSI/IEEE 1059 standard โ A process of analyzing a software item to detect the differences between existing and required conditions (i.e., defects) and to evaluate the features of the software item.
Testing is crucial for the success of your project, to ensure the quality of your code and identify issues on it before it would go to production because otherwise your code would break in any moment and give you headaches at midnight of Friday ๐ฑ , the best way to be sure that it won't happen is testing, it'll give you the confidence that your code is working as you expected and free of issues.
There are some types of automated test, Unit test, Integration, End to end, testing types ... today we are just going to focus on Integration, which is in a midway between end to end, and unit test, it doesn't run as fast as unit tests, but it will give you more confidence than unit tests and will run faster than end to end testing
Set up
for this post we are going to use the GraphQL API from one of my lasts post if you already have it, it's ok, otherwise, you can clone it
Dependencies
git clone https://github.com/Wonder2210/graphql-typescript-pg-server.git
and checkout to the commit before the tests
git checkout bdcf9de8b42ce81eaa2d11683be1e399e030b7ed
then we need to install these packages
yarn add -D jest @jagi/jest-transform-graphql ts-jest apollo-graphql-test knex-cleaner
we'll use Jest as our test runner and apollo-graphql-test to test our queries, it creates a client to execute it and knex-cleaner to clean the database once we run all the tests, with we'll be sure that every time we run the tests we'll have the same result or in other words, the tests are replicable.
Note*: knex-cleaner doesn't have support for typescript so we have to add the following line to index.d.ts
declare module "knex-cleaner"
Jest set up
first of all, we need a folder to store our tests and everything need it to run it
mkdir /src/__tests__
mkdir /src/__tests__/utils
Jest need a configuration to know which files it's going to run, where are they, and how to handle some files like our schema.gql
module.exports = {
testPathIgnorePatterns: ["/node_modules"],
testMatch: ["**/?(*.)+(spec|test).[jt]s?(x)"],
transform: {
"^.+\\.(j|t)sx?$": ["ts-jest"],
"\\.(gql|graphql)$": ["@jagi/jest-transform-graphql"],
},
moduleFileExtensions: ["ts", "tsx", "js", "gql"],
};
in this case, we are specifying the files to test with testMatch this indicates that will run every file with .test.ts or jsx or tsx ... and how will process some files, due that jest is native for javascript we need to transform our ts files (with ts-jest), and the .gql or .graphql files (with @jagi/jest-transform-graphql)
and we are going to add the following line to package.json
...
"scripts":{
...
"test:integration":"jest /src -c jest.config.js --forceExit"
...
}
...
Database
We are going to use a database for our tests, you can use your development database but is better a database just for these tasks
and insert some data in our test database, for that we'll use the seeds from Knex, which is an automatized/programmatically way of insert data in your database
// src/database/seeds/baseSeed.js
exports.seed = function (knex) {
// Deletes ALL existing entries
return knex("users")
.del()
.then(function () {
// Inserts seed entries
return knex("users").insert([
{ full_name: "luis", country_code: 58 },
{ full_name: "jose", country_code: 59 },
{ full_name: "raul", country_code: 39 },
]);
})
.then(() => {
return knex("pets")
.del()
.then(function () {
// Inserts seed entries
return knex("pets").insert([
{ name: "spot", owner_id: 1, specie: "MAMMALS" },
{ name: "chief", owner_id: 2, specie: "MAMMALS" },
{ name: "king", owner_id: 3, specie: "MAMMALS" },
]);
});
});
};
Also, we need to add the unsigned attribute to the owner_id field of pet, and our migration to create the pets table will look like
/migrations/initial_migration.ts
...
.createTable("pets", (table: Knex.CreateTableBuilder) => {
table.increments("id");
table.string("name");
table.integer("owner_id").references("users.id").unsigned().onDelete("CASCADE");
table.string("specie");
table.timestamps(true, true);
});
...
now we are going to add the following line to package.json top run the seeds
...
"scripts":{
...
"test:migrate:up": "knex --knexfile ./src/database/knexfile.ts migrate:latest --env testing",
"test:seed-db": "knex --knexfile ./src/database/knexfile.ts seed:run --env testing"
...
}
...
if you need it here is a template to write your own seeds
Test server
to test we need a server to serve our queries
// /src/__tests__/utils.server.ts
import { ApolloServer, Config } from "apollo-server-express";
import Knex from "knex";
import { Model } from "objection";
import dbconfig from "../../database/config";
import DataLoader from "dataloader";
import schema from "../../schema";
import { Pets, Users } from "../../utils/loaders";
import dbCleaner from "knex-cleaner";
const db = Knex(dbconfig["testing"]);
export const startDb = () => {
Model.knex(db);
};
export const stopDB = () => {
Model.knex().destroy();
};
export const cleanDb= async ()=>{
const options = {
mode: 'truncate',
restartIdentity: true,
}
return await dbCleaner.clean(db,options);
}
const config: Config = {
schema: schema,
context: {
loaders: {
users: new DataLoader(Users),
pets: new DataLoader(Pets),
},
},
};
export const server: () => ApolloServer = () => new ApolloServer(config);
Queries
Graphql use queries to retrieve data, so this is what we are gonna need to test it, Queries, this is something that you already use in GraphQl playground, but now we need those in a file to be used in our automated tests like that :
/__tests__/utils/queries.ts
import gql from "graphql-tag";
export const GET_USERS = gql`
query getUsers {
users {
id
full_name
country_code
}
}
`;
export const GET_USERS_AND_PETS = gql`
query getUsersAndItsPets {
users {
id
full_name
country_code
pets {
id
name
specie
}
}
}
`;
export const GET_PETS = gql`
query getPets {
pets {
id
name
owner_id
specie
}
}
`;
export const CREATE_USER = gql`
mutation CreateUser($name: String!, $country_code: String!) {
createUser(user: { full_name: $name, country_code: $country_code }) {
id
full_name
country_code
}
}
`;
export const CREATE_PET = gql`
mutation CreatePet($name: String!, $owner_id: Int!, $specie: Species!) {
createPet(pet: { name: $name, owner_id: $owner_id, specie: $specie }) {
id
name
specie
}
}
`;
export const DELETE_PET = gql`
mutation DeletePet($id: Int!) {
deletePet(id: $id) {
String
}
}
`;
export const DELETE_USER = gql`
mutation DeleteUser($id: Int!) {
deleteUser(id: $id)
}
`;
export const UPDATE_USER = gql`
mutation UpdateUser($id: Int!, $name: String, $country_code: String) {
updateUser(
user: { id: $id, full_name: $name, country_code: $country_code }
) {
id
full_name
country_code
}
}
`;
export const UPDATE_PET = gql`
mutation UpdatePet($id: Int!, $name: String!) {
updatePet(pet: { id: $id, name: $name }) {
id
name
owner_id
}
}
`;
as can you see there is all (well almost ) the queries we use in our API
now we need a client to execute them.
Client
for the client, we'll be using apollo-server-testing, it will create a 'client' to let us execute our queries in the server
Let's write tests ๐
in this case, we are not going to test if field A contains B, well no exactly, we are going to use snapshots, with that we capture the result of our queries, and with it, we can just review the results from the server, and check if they are OK or if something went broke when we are doing snapshot testing the test will be successful if match with the last snap, so if you add a new feature or your data has changed you'll have to update it the snapshots or the test will fail.
first of all, we import our queries and libraries needed and set up the actions to be realized before and after the execution of the tests
import { server, startDb, cleanDb } from "./utils/server";
import { createTestClient } from "apollo-server-testing";
import {
GET_USERS,
GET_PETS,
GET_USERS_AND_PETS,
CREATE_USER,
CREATE_PET,
UPDATE_USER,
DELETE_USER,
} from "./utils/queries";
beforeAll(() => {
return startDb(); // here we are starting the database before the tests run
});
afterAll(()=>{
return cleanDb(); // with that we are cleaning the database after all the tests have been executed
})
and now our tests will be like :
test("query users", async () => {
const serverTest = server();// start our tests server
const { query } = createTestClient(serverTest); // start our client passing our server as parameter and taking the query function from it
const res = await query({ query: GET_USERS });// we execute our query
expect(res).toMatchSnapshot();// we get the result and create or compare the snapshot
});
our whole tests will look like :
// /src/__tests__/integration.test.ts
import { server, startDb, cleanDb } from "./utils/server";
import { createTestClient } from "apollo-server-testing";
import {
GET_USERS,
GET_PETS,
GET_USERS_AND_PETS,
CREATE_USER,
CREATE_PET,
UPDATE_USER,
DELETE_USER,
} from "./utils/queries";
beforeAll(() => {
return startDb(); // here we are starting the database before every test run
});
afterAll(()=>{
return cleanDb(); // with that we are cleaning our database after all the tests have run
})
test("query users", async () => {
const serverTest = server();
const { query } = createTestClient(serverTest);
const res = await query({ query: GET_USERS });
expect(res).toMatchSnapshot();
});
test("query pets", async () => {
const serverTest = server();
const { query } = createTestClient(serverTest);
const res = await query({ query: GET_PETS });
expect(res).toMatchSnapshot();
});
test("query users and its pets", async () => {
const serverTest = server();
const { query } = createTestClient(serverTest);
const res = await query({ query: GET_USERS_AND_PETS });
expect(res).toMatchSnapshot();
});
test("create user ", async () => {
const serverTest = server();
const { mutate } = createTestClient(serverTest);
const res = await mutate({
mutation: CREATE_USER,
variables: {
name: "Wonder",
country_code: "ve",
},
});
expect(res).toMatchSnapshot();
});
test("create pet", async () => {
const serverTest = server();
const { mutate } = createTestClient(serverTest);
const res = await mutate({
mutation: CREATE_PET,
variables: {
name: "Optimus",
owner_id: 1,
specie: "MAMMALS",
},
});
expect(res).toMatchSnapshot();
});
test("update user", async () => {
const serverTest = server();
const { mutate } = createTestClient(serverTest);
const res = await mutate({
mutation: UPDATE_USER,
variables: {
id: 1,
name: "wilson",
},
});
expect(res).toMatchSnapshot();
});
test("delete user", async () => {
const serverTest = server();
const { mutate } = createTestClient(serverTest);
const res = await mutate({
mutation: DELETE_USER,
variables: {
id: 2,
},
});
expect(res).toMatchSnapshot();
});
So finally that's what we are going to do to execute our tests for the first time
1) yarn run test:migrate:up to generate the tables
2) yarn run test:seed-db to fill with data the database
3) yarn run test:integration to run our tests
Now to run it for the second time we need to run
1) yarn run test:seed-db
2) yarn run test:integration
and you'll have an output like this
Thanks for reading , please follow me here and on twitter ๐, any doubt , suggestion, or correction please let me know in a comment below ๐
Top comments (3)
Hey, nice article :)
I also wrote one more focused on the security side that's a nice follow up to this. It doesn't go into the detail as you did about the actual tests, instead it's focused on how to run those tests when the API is secured: it dev.to/mestrak/testing-secure-apis...
thank you, you are on the right time, I was figuring out how to test that implementation !!
Great, hope it's useful!