Draft Node.js API Testing Guide

Create a guide for how and what to test on a Node.js API.

Category: Node.js Web Applications Last Updated: 09/26/19

Github: https://github.com/TechandStartup/node-api-testing


Goal: Provide curated standardized building blocks that software developers can trust to use out of the box, and can easily customize to meet the requirements of their projects.
Skill level: Applies to anyone who is or wants to be a web developer. Applies to all skill levels, from brand new programmers to experienced Node.js web devs.
Simple Sites: Non programmers who want to build a simple website for their small business or organization would be better served using Wordpress. It doesn't require any programming, and you can have a basic site up within hours. It is too slow at high volume however.
This is a Guide: Not meant to be a tutorial per se, but rather a guide that can be used as a starting point or template for building a web app.
Opinionated yet flexible: For each decision point a recommended approach is taken after weighing the pros and cons of the options. But because these are just guides you can modify whatever you like thus providing the best of both worlds.
Evaluation criteria: Each approach option is weighed with the following criteria in mind: performance, popularity, maturity, clarity, simplicity, consistency, intuitiveness, beginner friendliness, conventions, best practices, rolling your own favored over installing packages, favoring the core web languages (JavaScript, JSON, HTML, CSS) over DSLs (domain specific languages) or other programming languages, node package documentation.
Up-to-date: Each guide is reviewed when new major or minor versions of dependency packages are released and updates are made where appropriate.


Test Setup

Mocha Docs: Getting Started Chai Docs: Guide | API Chai: Chai is an assertion library, similar to Node's built-in assert. Mocha: Mocha is a JavaScript testing framework running on Node.js and in the browser. Install the chai, chai-http, and mocha packages. npm install --save-dev chai chai-http mocha Create a tests directory and a file to test the articles collection. mkdir tests touch tests/articles.js

Create a test database

Make sure MongoDB is running. mongod Open the MongoDB shell. mongo Create the test database. The use command creates the database if it doesn't already exist. use my_test_db Add the test database URI to the .env file. // .env MONGODB_URI=mongodb://localhost:27017/my_local_db MONGODB_TEST_URI=mongodb://localhost:27017/my_test_db Change the database url in the mongoose.connect method to depend on the environment. And export the server so it can be imported in the test file(s). // server.js mongoose.connect( // Check if environment is 'test' to see what db uri to connect to. process.env.NODE_ENV === 'test' ? process.env.MONGODB_TEST_URI : process.env.MONGODB_URI, { useNewUrlParser: true, useFindAndModify: false, useCreateIndex: true, useUnifiedTopology: true } ); ... module.exports = app; Add a script to the package.json file that sets the NODE_ENV environmental variable to test and runs the mocha command to execute the files in the tests folder. // package.json "scripts": { "start": "node server.js", "test": "NODE_ENV=test mocha 'tests/*.js'" },

Open Items

Are Mocha + Chai the right testing packages to use? • This seems to be the most popular testing combination for Node.js apps. • Jest is a newer option that is an all in one package. But it was created for React on the client side and may not yet be optimal for Node on the server side. • The Mongoose.js docs recommend using Mocha + Chai and recommend against Jest.

Test Assertions

Add the tests // tests/articles.js const mongoose = require("mongoose"); const Article = require('../models/article'); const chai = require('chai'); const chaiHttp = require('chai-http'); const server = require('../server'); const should = chai.should(); chai.use(chaiHttp); let article1 = Article.create({title: "Learn Testing", content: "Lorem Ipsum", published: true}) describe('Articles', () => { after(async () => { await Article.deleteMany() }) describe('POST /articles/create', () => { it('it should not create an article without a title', (done) => { let article = { content: "Blah blah", published: true } chai.request(server) .post('/articles/create') .send(article) .end((err, res) => { res.should.have.status(422); res.body.should.be.a('object'); res.body.should.have.property('errors'); res.body.errors[0].should.have.property('msg').eql('Title is required'); done(); }); }); it('it should create an article ', (done) => { let article = { title: "Learn Mocha", content: "Test this.", published: true } chai.request(server) .post('/articles/create') .send(article) .end((err, res) => { res.should.have.status(200); res.body.should.be.a('object'); res.body.should.have.property('message').eql('Article successfully created'); res.body.article.should.have.property('title'); res.body.article.should.have.property('content'); res.body.article.should.have.property('published'); done(); }); }); }); describe('GET /articles/:id', () => { it('it should GET article with given id', (done) => { let article = new Article({title: "Learn Chai", content: "Lorem Ipsum", published: true}) article.save((err, article) => { chai.request(server) .get('/articles/' + article._id) .end((err, res) => { res.should.have.status(200); res.body.should.be.a('object'); res.body.should.have.property('title').eql("Learn Chai"); done(); }); }); }); }); describe('GET /articles', () => { it('it should GET all the articles', (done) => { chai.request(server) .get('/articles') .end((err, res) => { res.should.have.status(200); res.body.should.be.a('array'); res.body.length.should.be.eql(3); done(); }); }); }); describe('/PATCH /articles/:id/update', () => { it('it should update a article given an id', (done) => { let article = new Article({title: "Learn Testing", content: "Lorem Ipsum", published: true}) article.save((err, article) => { chai.request(server) .patch('/articles/' + article._id + '/update') .send({title: "Learn TDD", content: "Blah blah."}) .end((err, res) => { res.should.have.status(200); res.body.should.be.a('object'); res.body.should.have.property('message').eql('Article updated'); res.body.article.should.have.property('title').eql("Learn TDD"); done(); }); }); }); }); describe('DELETE /articles/:id/delete', () => { it('it should delete a article given an id', (done) => { let article = new Article({title: "Learn Unit Testing", content: "Lorem Ipsum", published: true}) article.save((err, article) => { chai.request(server) .delete('/articles/' + article.id + '/delete') .end((err, res) => { res.should.have.status(200); res.body.should.be.a('object'); res.body.should.have.property('message').eql('Article deleted'); done(); }); }); }); }); });

Open Items

Should you use the assert, expect, or should syntax? • This seems just a matter of preference. No performance difference. ToDo: Automated way to seed the database at the start of each test run. ToDo: Add a consistent tear-down process for clearing the database. • Right now using the after method in the articles test file to delete all articles after the tests in those files are run. ToDo: What to test when testing a list action. ToDo: What to test when testing a details action. ToDo: What to test when testing a create action. ToDo: What to test when testing an update action. ToDo: What to test when testing a delete action.

Run Tests

Make sure MongoDB is running mongod And the app in the development environment is stopped CTRL+C Run the test script in the package.json file. npm test To exit the test environment: CTRL+C