Add Integration Tests to Mongoose blogging app
by John Vincent
Posted on April 20, 2017
Building Integration Tests using Fake, Mocha, Chai for a Mongo-backed Express app.
Now it's time to practice writing integration tests for MongoDB backed Express apps.
To complete this challenge, we'd like you to add integration tests for all 4 of the API endpoints. Your integration tests should use the strategy described in the previous assignment (set up db in known state, make a request to API, inspect response, inspect state of db, tear down db). At a minimum, you should write tests for the normal case for each endpoint.
From Thinkful course Node 2.3.2.
Final Result
Getting Started
Created repository: mongoose-challenge-with-tests
Copied:
/Users/jv/Desktop/MyDevelopment/github/thinkful/mongoose-challenge
to:
/Users/jv/Desktop/MyDevelopment/github/mongoose-challenge-with-tests
Rebuild package.json
rm package.json
npm init
npm install express --save
npm install mongoose --save
npm install body-parser --save
npm install morgan --save
npm install uuid --save
npm install mocha --save-dev
npm install chai --save-dev
npm install chai-http --save-dev
npm install faker --save-dev
package.json
"main": "server.js",
"scripts": {
"start": "node server.js",
"test": "mocha ./test"
},
Test
Start mongo and run tests:
npm start
http://localhost:8080/blog
retrieves records.
Test Using Mocha
npm test
Test Using Devtools
devtool ./node_modules/mocha/bin/_mocha ./test
Development
config.js
exports.TEST_DATABASE_URL = (process.env.TEST_DATABASE_URL || 'mongodb://localhost/test-blogDB');
test-blog.js
const faker = require('faker');
const mongoose = require('mongoose');
const {TEST_DATABASE_URL} = require('../config');
const {BlogModel} = require('../models');
function generateData() {
return {
title: faker.lorem.words(),
content: faker.lorem.sentence(),
author: {
firstName: faker.name.firstName(),
lastName: faker.name.lastName()
},
created: faker.date.past()
};
}
function seedData() {
console.info('seeding blog data');
const data = [];
for (let i=1; i<=10; i++) {
data.push(generateData());
}
// this will return a promise
return BlogModel.insertMany(data);
}
function tearDownDb() {
console.warn('Deleting database');
return mongoose.connection.dropDatabase();
}
before(function() {
return runServer(TEST_DATABASE_URL);
});
beforeEach(function() {
return seedData();
});
afterEach(function() {
return tearDownDb();
});
after(function() {
return closeServer();
});
Test using Mocha
npm test
test-blog.js
/* jshint node: true */
/* jshint esnext: true */
/* global describe, it, before, after, beforeEach, afterEach */
'use strict';
const chai = require('chai');
const chaiHttp = require('chai-http');
const faker = require('faker');
const mongoose = require('mongoose');
const {app, runServer, closeServer} = require('../server');
const {TEST_DATABASE_URL} = require('../config');
const {BlogModel} = require('../models');
/* jshint -W098 */
const should = chai.should();
chai.use(chaiHttp);
function generateData() {
return {
title: faker.lorem.words(),
content: faker.lorem.sentence(),
author: {
firstName: faker.name.firstName(),
lastName: faker.name.lastName()
},
created: faker.date.past()
};
}
function seedData() {
console.info('seeding blog data');
const data = [];
for (let i=1; i<=10; i++) {
data.push(generateData());
}
// this will return a promise
return BlogModel.insertMany(data);
}
function tearDownDb() {
console.warn('Deleting database');
return new Promise((resolve, reject) => {
console.warn('Deleting database');
mongoose.connection.dropDatabase()
.then(result => resolve(result))
.catch(err => reject(err));
});
// return mongoose.connection.dropDatabase();
}
describe('Blogs API resources', function() {
before(function() {
return runServer(TEST_DATABASE_URL);
});
beforeEach(function() {
return seedData();
});
afterEach(function() {
return tearDownDb();
});
after(function() {
return closeServer();
});
describe('GET endpoint', function() {
/*
strategy:
1. get back all blogs returned by by GET request to `/blog`
2. prove res has right status, data type
3. prove the number of blogs we got back is equal to number in db.
*/
let res;
it('should return all blogs', function() {
return chai.request(app)
.get('/blog') // 1
.then(function(_res) { // 2
res = _res;
res.should.have.status(200);
res.body.length.should.be.at.least(1);
return BlogModel.count();
})
.then(function(count) { // 3
res.body.should.have.length.of(count);
});
});
/*
strategy:
1. get back all blogs returned by by GET request to `/blog`
2. prove res has right status, data type
3. get first document from the database
4. verify fields have correct values
*/
it('should return fields with correct values', function() {
let item;
return chai.request(app)
.get('/blog') // 1
.then(function(res) {
res.should.have.status(200); // 2
/* jshint -W030 */
res.should.be.json;
res.body.should.be.a('array');
res.body.length.should.be.at.least(1);
const expectedKeys = ['id', 'title', 'content', 'author', 'created'];
res.body.forEach(function(item) {
item.should.be.a('object');
item.should.include.keys(expectedKeys);
});
item = res.body[0]; // first record
return BlogModel.findById(item.id).exec(); // 3
})
.then(function(blog) { // 4
blog.id.should.equal(item.id);
blog.title.should.equal(item.title);
blog.content.should.equal(item.content);
item.author.should.equal(`${blog.author.firstName} ${blog.author.lastName}`);
blog.created.toJSON().should.equal(item.created); // json formatted ISO date
});
});
});
describe('GET by ID endpoint', function() {
/*
strategy:
1. find one record
2. get that record by id
3. prove res has right status, data type
4. verify fields have correct values
*/
it('should get one blog by id', function() {
let item;
return BlogModel // 1
.findOne()
.exec()
.then(function(blog) {
item = blog;
return chai.request(app)
.get('/blog/'+item.id); // 2
})
.then(function(res) {
res.should.have.status(200); // 3
res.body.should.be.a('object');
res.body.should.include.keys('id', 'title', 'content', 'author', 'created');
res.body.id.should.equal(item.id); // 4
res.body.title.should.equal(item.title);
res.body.content.should.equal(item.content);
res.body.author.should.equal(`${item.author.firstName} ${item.author.lastName}`);
res.body.created.should.equal(item.created.toJSON()); // json formatted ISO date
});
});
/*
strategy:
1. Create a non-existent Id
2. get that record by id
3. verify status
*/
it('should fail to get record by a bad id', function() {
let myid = mongoose.Types.ObjectId(); // 1
return chai.request(app)
.get(`/blog/${myid}`) // 2
.then(function(res) {
res.should.have.status(204); // 3
});
});
});
describe('POST endpoint', function() {
/*
strategy:
1. create a new record
2. post the record
3. prove res has right status, data type
4. verify fields have correct values
5. get the record by id
6. verify record is identical to the new record.
*/
it('should add a new blog', function() {
let created;
const item = generateData(); // 1
return chai.request(app)
.post('/blog') // 2
.send(item)
.then(function(res) {
res.should.have.status(201); // 3
/* jshint -W030 */
res.should.be.json;
res.body.should.be.a('object');
res.body.should.include.keys('id', 'title', 'content', 'author', 'created');
res.body.id.should.not.be.null;
res.body.title.should.equal(item.title); // 4
res.body.content.should.equal(item.content);
res.body.author.should.equal(`${item.author.firstName} ${item.author.lastName}`);
created = res.body.created;
return BlogModel.findById(res.body.id).exec(); // 5
})
.then(function(blog) { // 6
blog.title.should.equal(item.title);
blog.content.should.equal(item.content);
blog.author.firstName.should.equal(item.author.firstName);
blog.author.lastName.should.equal(item.author.lastName);
blog.created.toJSON().should.equal(created); // json formatted ISO date
});
});
});
describe('POST endpoint error conditions', function() {
/*
strategy:
1. create a new record missing title field
2. make a POST request with that record
3. ensure status code = 400
*/
it('should fail to add a new blog - missing title', function() {
const newBlog = generateData();
delete newBlog.title;
chai.request(app)
.post('/blog')
.send(newBlog)
.then(() => {
throw Error('should have failed with a 400');
})
.catch(e => {
e.status.should.equal(400);
});
});
/*
strategy:
1. create a new record missing content field
2. make a POST request with that record
3. ensure status code = 400
*/
it('should fail to add a new blog - missing content', function() {
const newBlog = generateData();
delete newBlog.content;
chai.request(app)
.post('/blog')
.send(newBlog)
.then(() => {
throw Error('should have failed with a 400');
})
.catch(e => {
e.status.should.equal(400);
});
});
/*
strategy:
1. create a new record missing firstName field
2. make a POST request with that record
3. ensure status code = 400
*/
it('should fail to add a new blog - missing firstName', function() {
const newBlog = generateData();
delete newBlog.author.firstName;
chai.request(app)
.post('/blog')
.send(newBlog)
.then(() => {
throw Error('should have failed with a 400');
})
.catch(e => {
e.status.should.equal(400);
});
});
/*
strategy:
1. create a new record missing lastName field
2. make a POST request with that record
3. ensure status code = 400
*/
it('should fail to add a new blog - missing lastName', function() {
const newBlog = generateData();
delete newBlog.author.lastName;
chai.request(app)
.post('/blog')
.send(newBlog)
.then(() => {
throw Error('should have failed with a 400');
})
.catch(e => {
e.status.should.equal(400);
});
});
});
describe('PUT endpoint', function() {
/*
Strategy:
1. Get an existing blog
2. Put request to update the data.
3. Prove blog data returned by request is the same data used in the update.
4. Get record from database by id
5. Prove blog data in database is the same data used in the update.
*/
it('should update fields you send over', function() {
const updateData = generateData();
return BlogModel // 1
.findOne()
.exec()
.then(function(blog) {
updateData.id = blog.id;
updateData.created = blog.created;
return chai.request(app)
.put(`/blog/${updateData.id}`) // 2
.send(updateData);
})
.then(function(res) {
res.should.have.status(201);
/* jshint -W030 */
res.should.be.json;
res.body.should.be.a('object');
res.body.should.include.keys('id', 'title', 'content', 'author', 'created');
res.body.id.should.equal(updateData.id); // 3
res.body.title.should.equal(updateData.title);
res.body.content.should.equal(updateData.content);
res.body.author.should.equal(`${updateData.author.firstName} ${updateData.author.lastName}`);
res.body.created.should.equal(updateData.created.toJSON());
return BlogModel.findById(updateData.id).exec(); // 4
})
.then(function(blog) { // 5
blog.id.should.equal(updateData.id);
blog.title.should.equal(updateData.title);
blog.content.should.equal(updateData.content);
blog.author.firstName.should.equal(updateData.author.firstName);
blog.author.lastName.should.equal(updateData.author.lastName);
blog.created.toJSON().should.equal(updateData.created.toJSON());
});
});
});
describe('DELETE endpoint', function() {
/*
Strategy:
1. Get one record
2. make a delete request for that record's id
3. assert that response has correct status code
4. get the record by id
5. prove that record does not exist
*/
it('should delete by id', function() {
let item;
return BlogModel // 1
.findOne()
.exec()
.then(function(blog) {
item = blog;
return chai.request(app).delete(`/blog/${blog.id}`); // 2
})
.then(function(res) {
res.should.have.status(204); // 3
return BlogModel.findById(item.id).exec(); // 4
})
.then(function(_blog) { // 5
should.not.exist(_blog);
});
});
/*
Strategy:
1. Create a non-existent Id
2. make a delete request for that id
3. assert that response has correct status code
*/
it('delete blog by non-existent id', function() {
let myid = mongoose.Types.ObjectId(); // 1
return chai.request(app).delete(`/blog/${myid}`) // 2
.then(res => {
res.should.have.status(204); // 3
});
});
});
});
blogRouter.js
needed a change for record not found:
.then(blog => {
if (blog) {
res.json(blog.getAll());
}
else {
res.status(204).json({message: `Record ${req.params.id} not found`});
}
})