DEV Community

loading...
Cover image for How I'm currently testing my GraphQL Resolvers & Mongoose Operations

How I'm currently testing my GraphQL Resolvers & Mongoose Operations

José Del Valle
Hello! I am a Software Engineer from Costa Rica, specialized in Web Development. Currently, a self-employed developer working on my own projects and my blog: https://delvalle.dev
・3 min read

Photo by Jorn Eriksen on Unsplash.

During the last couple of days, I've been doing some integration testing on my resolvers plus my mongoose operations.

I thought it would be nice to just share the code. If anyone has some feedback or would like to suggest a different approach feel free to do so.

I'm using Jest as the test runner. I added this simple config so it doesn't complain cause I'm on Node instead of the client-side:

module.exports = {
  testEnvironment: 'node'
};

Here's my test script in package.json

"test": "env-cmd -e test -r ./.env-cmdrc.json jest",

I use env-cmd to manage my environment variables but this can also be as simple as:

"test": "jest",

Then I have a setup file where I create the test server using apollo-server-testing. I also import the graphql resolvers and typedefs, and my mongoose models to pass in the context function of the Apollo Server.

Then I also have some simple functions to connect, disconnect and drop the test DB. The MONGO_URI env variable just points to a test DB that is created before the tests and dropped when they're done.

const { createTestClient } = require('apollo-server-testing');
const { ApolloServer } = require('apollo-server-express');
const mongoose = require('mongoose');

const {
  typeDefs,
  resolvers
} = require('../schema');
const Device = require('../models/Device');
const DeviceSetting = require('../models/DeviceSetting');


const connectToDb = async () => {
  await mongoose.connect(process.env.MONGO_URI, 
    { useNewUrlParser: true, useUnifiedTopology: true }).catch(error => console.error(error));;
}

const dropTestDb = async () => {
  if(process.env.NODE_ENV === 'test'){
    await mongoose.connection.db.dropDatabase().catch(error => console.error(error));;
  }
}

const closeDbConnection = async () => {
  await mongoose.connection.close().catch(error => console.error(error));;
}


const server = new ApolloServer({
  typeDefs,
  resolvers,
  context: ({
    req,
    res
  }) => ({
    req,
    res,
    Device,
    DeviceSetting,
  }),
});

module.exports = {
  testClient: createTestClient(server),
  connectToDb,
  closeDbConnection,
  dropTestDb
}

And here are my tests. I'm currently testing all the CRUD operations, such as add, get, get all, update and delete. I do some snapshot testing but not for every case. The tests are somewhat dependant on the previous ones so that's why I store some ids to use them in further tests:

const { gql } = require('apollo-server-express');
const { testClient, connectToDb, dropTestDb, closeDbConnection } = require('../__testSetup__/setup');
const { ObjectId } = require('mongodb');

const { query, mutate } = testClient;

beforeAll(async () => {
  await connectToDb()
  await dropTestDb()
});

afterAll(async () => {
  await dropTestDb()
  await closeDbConnection()
});

describe("Device Resolvers", () => {

  const user = "5e9affa8d8f5db0461906ac7";
  let deviceId = '';

  it("Add Device", async () => {
    const ADD_DEVICE = gql`
      mutation addDevice( $user: ID!, $model: String, $brand: String, $description: String, $image: String) {
        addDevice(user: $user, model: $model, brand: $brand, description: $description, image: $image) {
          user
          model
          brand
          description
          image
        }
      }
    `
    const device = { 
      user,
      model: "Model name",
      brand: "The Brand",
      description: "Description",
      image: "url/to/image"
    }

    const { data } = await mutate({
      mutation: ADD_DEVICE,
      variables: { 
        ...device
      }
    })

    expect(data).toEqual({
      addDevice: {
        ...device
      }
    });

  });

  it("Get User Devices", async () => {
    const GET_USER_DEVICES = gql`
      query getUserDevices($user: ID!) {
        getUserDevices(user: $user) {
          _id
          user
          model
          brand
          description
          likes
          image
        }
      }
    `
    const { data } = await query({
      mutation: GET_USER_DEVICES,
      variables: { 
        user
      }
    });

    const { getUserDevices } = data;

    const expectedDevices = getUserDevices.map(device => {
      const { _id, ...rest } = device;
      expect(ObjectId.isValid(_id)).toEqual(true);
      expect(ObjectId.isValid(rest.user)).toEqual(true);
      return rest;
    })

    expect(expectedDevices).toMatchSnapshot();

    deviceId = getUserDevices[0]._id

  });

  it("Get Device", async () => {
    const GET_DEVICE = gql`
      query getDevice($_id: ID!) {
        getDevice(_id: $_id) {
          _id
          user
          model
          brand
          description
          likes
          image
        }
      }
    `

    const { data } = await query({
      mutation: GET_DEVICE,
      variables: { 
        _id: deviceId
      }
    })

    expect(ObjectId.isValid(data.getDevice._id)).toEqual(true);
    expect(ObjectId.isValid(data.getDevice.user)).toEqual(true);

    const { _id, ...rest } = data.getDevice;
    expect(rest).toMatchSnapshot();
  });

  it("Update Device Info", async () => {

    const UPDATE_DEVICE_INFO = gql`
      mutation updateDeviceInfo($_id: ID!, $model: String, $brand: String, $description: String, $image: String) {
        updateDeviceInfo(_id: $_id, model: $model, brand: $brand, description: $description, image: $image) {
          user
          model
          brand
          description
          image
        }
      }
    `
    const newInfo = { 
      _id: deviceId,
      model: "Updated model name",
      brand: "Another Brand",
      description: "Another Description",
      image: "url/to/image/2"
    }

    const { data } = await mutate({
      mutation: UPDATE_DEVICE_INFO,
      variables: { 
        ...newInfo
      }
    });

    const { _id, ...info } = data.updateDeviceInfo;

    expect(data).toEqual({
      updateDeviceInfo: {
        user,
        ...info
      }
    });

  });

  it("Delete Device", async () => {

    const DELETE_DEVICE = gql`
      mutation deleteDevice($_id: ID!) {
        deleteDevice(_id: $_id) {
          _id
        }
      }
    `

    const { data } = await mutate({
      mutation: DELETE_DEVICE,
      variables: { 
        _id: deviceId
      }
    });

    expect(data).toEqual({
      deleteDevice: {
        _id: deviceId
      }
    });

  });

});

So, as you can see. This is just like doing queries and mutations as you would normally do in the app. Maybe extracting the gql definitions would make this cleaner.

What I want to test here is primarily that everything is being stored, retrieved, updated and deleted to and from the database. Also that the results of these operations are good.

This is the first time I'm doing integration tests on this particular stack so any feedback is more than welcome.

I hope this was useful and thanks for reading!

Follow me on twitter: @jdelvx

Discussion (2)

Collapse
mucorolle profile image
Muco Rolle Tresor

As someone who has never done any automation testing this was really helpful and thanks for giving me the motivation for writing tests

Collapse
aks30498 profile image
aks30498

Why do I always hear from people that I should just mock the DB calls ? What would be the approach in that case ?