Skip to content

Instantly share code, notes, and snippets.

@cklanac
Last active November 3, 2018 08:56
Show Gist options
  • Save cklanac/c4538387267d36580f881639bcc2d31f to your computer and use it in GitHub Desktop.
Save cklanac/c4538387267d36580f881639bcc2d31f to your computer and use it in GitHub Desktop.
Challenge 13: Mongoose Testing

In this challenge you will add integration tests to your API. The goal is to create tests which cover all the correct/positive scenarios as well as tests for negative scenarios such as a 404 Not Found.

Requirements

  • NPM install devDependencies for testing and update package.json file
  • Update server.js to prevent mongoose.connect and app.listen from running during tests
  • Export app for testing
  • Update config.js with the test database URL
  • Create a /test/notes.test.js file and tests for each endpoint

Install NPM Dev-Dependencies for testing

You need several packages for testing which are included in the starter. But you should ensure they are correct. Double check the package.json for the following packages.

  • mocha the testing framework
  • chai the assertion library
  • chai-http a chai plug-in which allows you to make http requests to the API
  • cross-env which provides a convenient way to set environment variables across platforms

If they are not installed, you can install as follows.

npm install mocha chai chai-http cross-env --save-dev

Remember to add the --save-dev flag so the packages are saved under devDependencies.

Configure NPM script to run tests

Add "test": "mocha" to your scripts property in package.json. Notice, the cross-env command will set the NODE_ENV to test when run your test suite using the npm test command.

  "scripts": {
    "start": "node server.js",
    "test": "cross-env NODE_ENV=test mocha --file test/server.js"
  },

Update the server.js

You need to prevent mongoose.connect and app.listen from executing when running tests. Wrap the calls in a process.env.NODE_ENV !== 'test' condition. And export app.

if (process.env.NODE_ENV !== 'test') {

  mongoose.connect(MONGODB_URI)
    .then(instance => { 
      REMOVED FOR BREVITY

  app.listen(PORT, function () { ...
    REMOVED FOR BREVITY
  }

module.exports = app; // Export for testing

Update the config.js

Next, in config.js, add an entry for the test database URL. Add the following to your module.exports object

  TEST_MONGODB_URI: process.env.TEST_MONGODB_URI || 'mongodb://localhost/noteful-test'

Create notes.test.js

Create a new test file /test/notes.test.js. Recall that Mocha will automatically run any file in the /test/ directory. At the top of the file, require the following packages.

  • Require
    • chai, chai-http and mongoose packages
    • app from server.js
    • config.js and destructure the TEST_MONGODB_URI into a constant
    • note Mongoose model for Notes
    • /db/seed/notes seed data

Finally, configure expect as your assertion library and load chai-http with chai.use()

The start of your file should look something like this:

const chai = require('chai');
const chaiHttp = require('chai-http');
const mongoose = require('mongoose');

const app = require('../server');
const { TEST_MONGODB_URI } = require('../config');

const Note = require('../models/note');

const seedNotes = require('../db/seed/notes');

const expect = chai.expect;
chai.use(chaiHttp);

Next, configure the Mocha hooks manage the database during the tests..

  • before(): connect to the database before all tests
  • beforeEach(): seed data runs before each test
  • afterEach(): drop database runs after each test
  • after(): disconnect after all tests

Add the following code to your script, place it inside a describe() which wraps your tests.

  before(function () {
    return mongoose.connect(TEST_MONGODB_URI)
      .then(() => mongoose.connection.db.dropDatabase());
  });

  beforeEach(function () {
    return Note.insertMany(seedNotes);
  });

  afterEach(function () {
    return mongoose.connection.db.dropDatabase();
  });

  after(function () {
    return mongoose.disconnect();
  });

Create Tests for each endpoint

Your challenge is to create tests for the Notes endpoints that check both positive (successful) and negative (error) scenarios.

Integration Test Recipes

To help get you started, here are a few sample tests which demonstrate different "recipes".

Serial Request - Call API then call DB then compare

  1. First, call the API to insert the document
  2. then call the database to retrieve the new document
  3. then compare the API response to the database results

Notice you need to set the res.body to body declared at a higher scope so that the next .then() has access to the value.

  describe('POST /api/notes', function () {
    it('should create and return a new item when provided valid data', function () {
      const newItem = {
        'title': 'The best article about cats ever!',
        'content': 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor...'
      };

      let res;
      // 1) First, call the API
      return chai.request(app)
        .post('/api/notes')
        .send(newItem)
        .then(function (_res) {
          res = _res;
          expect(res).to.have.status(201);
          expect(res).to.have.header('location');
          expect(res).to.be.json;
          expect(res.body).to.be.a('object');
          expect(res.body).to.have.keys('id', 'title', 'content', 'createdAt', 'updatedAt');
          // 2) then call the database
          return Note.findById(res.body.id);
        })
        // 3) then compare the API response to the database results
        .then(data => {
          expect(res.body.id).to.equal(data.id);
          expect(res.body.title).to.equal(data.title);
          expect(res.body.content).to.equal(data.content);
          expect(new Date(res.body.createdAt)).to.eql(data.createdAt);
          expect(new Date(res.body.updatedAt)).to.eql(data.updatedAt);
        });
    });
  });

Serial Request - Call DB then call API then compare:

  1. First, call the database to get an ID
  2. then call the API with the ID
  3. then compare database results to API response

Notice again, you need to capture the _data response and set it to the variable data declared at a higher scope so that the next .then() has access to the value.

  describe('GET /api/notes/:id', function () {
    it('should return correct note', function () {
      let data;
      // 1) First, call the database
      return Note.findOne()
        .then(_data => {
          data = _data;
          // 2) then call the API with the ID
          return chai.request(app).get(`/api/notes/${data.id}`);
        })
        .then((res) => {
          expect(res).to.have.status(200);
          expect(res).to.be.json;

          expect(res.body).to.be.an('object');
          expect(res.body).to.have.keys('id', 'title', 'content', 'createdAt', 'updatedAt');

          // 3) then compare database results to API response
          expect(res.body.id).to.equal(data.id);
          expect(res.body.title).to.equal(data.title);
          expect(res.body.content).to.equal(data.content);
          expect(new Date(res.body.createdAt)).to.eql(data.createdAt);
          expect(new Date(res.body.updatedAt)).to.eql(data.updatedAt);
        });
    });
  })

Parallel Request - Call both DB and API, then compare:

  1. Call the database and the API
  2. Wait for both promises to resolve using Promise.all
  3. then compare database results to API response

The advantage of this approach is that both responses are available in the same scope, so you do not need to set a variable at a higher scope. But this only works with GET endpoints because there are no DB changes performed.

  describe('GET /api/notes', function () {
    // 1) Call the database **and** the API
    // 2) Wait for both promises to resolve using `Promise.all`
    return Promise.all([
        Note.find(),
        chai.request(app).get('/api/notes')
      ])
      // 3) then compare database results to API response
        .then(([data, res]) => {
          expect(res).to.have.status(200);
          expect(res).to.be.json;
          expect(res.body).to.be.a('array');
          expect(res.body).to.have.length(data.length);
        });
  });

Continuous Integration and Deployment

Follow the Setup Travis and Heroku CICD: CLI Ninja to deploy your app to Heroku

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment