DEV Community

Wonder2210
Wonder2210

Posted on • Edited on

Integration Tests with GraphQL

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
Enter fullscreen mode Exit fullscreen mode

and checkout to the commit before the tests

git checkout bdcf9de8b42ce81eaa2d11683be1e399e030b7ed
Enter fullscreen mode Exit fullscreen mode

then we need to install these packages

yarn add -D jest @jagi/jest-transform-graphql ts-jest apollo-graphql-test knex-cleaner
Enter fullscreen mode Exit fullscreen mode

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" 
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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"],
};
Enter fullscreen mode Exit fullscreen mode

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"
    ...
}
...
Enter fullscreen mode Exit fullscreen mode

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" },
          ]);
        });
    });
};

Enter fullscreen mode Exit fullscreen mode

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);
    });
...
Enter fullscreen mode Exit fullscreen mode

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"
    ...
}
...
Enter fullscreen mode Exit fullscreen mode

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);

Enter fullscreen mode Exit fullscreen mode

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
    }
  }
`;

Enter fullscreen mode Exit fullscreen mode

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
})
Enter fullscreen mode Exit fullscreen mode

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 
});
Enter fullscreen mode Exit fullscreen mode

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();
});

Enter fullscreen mode Exit fullscreen mode

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
result

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)

Collapse
 
mestrak profile image
MeStrak

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...

Collapse
 
wonder2210 profile image
Wonder2210

thank you, you are on the right time, I was figuring out how to test that implementation !!

Collapse
 
mestrak profile image
MeStrak

Great, hope it's useful!