3 - Mongoose
We will now add Mongoose which is like a Object mapper for MongoDB.
npm install --save mongoose
We will then create a folder for our models
mkdir src/models
And lets go ahead and create our first model: User.js
import mongoose from 'mongoose'
const Schema = mongoose.Schema
const schema = Schema({
name: { type: String },
created_at: { type: Date, default: Date.now }
})
const User = mongoose.model('User', schema)
export default User
Testing with mockgoose
Lets create a test for it. We will use
- mockgoose: which will mock mongodb using an in-memory representation of the database
- chai-as-promised: to make it easier to test promises (since mongoose methods will return promises)
Run:
npm install mockgoose chai-as-promised --save-dev
And we need to setup some global configuration for all tests so go ahead and create a test/setup.js file
// chai
import chai from 'chai'
import chaiAsPromised from 'chai-as-promised'
chai.use(chaiAsPromised)
// mongoose
import mongoose from 'mongoose'
mongoose.Promise = global.Promise
And change the test script in package.json to include a "--require" of that file
"test": "export NODE_PATH=./ && mocha --require test/setup.js --compilers js:babel-register --recursive",
And now lets create a new folder for our tests
mkdir test/models
vi test/models/User_spec.js
And include the following content
import mongoose from 'mongoose'
import mockgoose from 'mockgoose'
import { expect } from 'chai'
import User from '../../src/models/User'
describe("User", ()=> {
before(done => {
mockgoose(mongoose).then(() => {
mongoose.connect('mongodb://example.com/TestingDB', done)
})
})
after(done => {
mockgoose.reset(done)
})
it("should be retrieved after saved", () => {
const user = new User({ name: 'pepito' })
return user.save()
.then(() => User.find()
.then(users => {
expect(users).not.to.be.equal(undefined)
expect(users.length).to.be.equal(1)
expect(users[0]).to.be.have.property('name', 'pepito')
})
)
})
})
Now run the tests again
Integration with Webapp
Now we will integrate our expressjs webapp with mongoose and provide and endpoint to retrieve users.
We will change the setup of server.js extending the idea that we have modeled for setting up express with the src/config/express.js. So we will consider that there could be many modules there that export a function(app):Promise[void] and perform some kind of initialization.
Lets edit server.js
import express from 'express'
import expressConfig from './config/express'
import mongoConfig from './config/express'
const app = express()
const setups = [expressConfig, mongoConfig]
setups.forEach(setup => setup(app))
// ... routes below
Then create a new file src/config/mongoose.js
// mongoose
import mongoose from 'mongoose'
mongoose.Promise = global.Promise
export * from '../models/all'
export default function (app) {
mongoose.connect('mongodb://localhost/myappdatabase')
}
The connection URL will be hardcoded for the moment.
Then in order to make it easier to load all the mongoose "models", lets create a file in src/models/all.js
// list of all models
export * from './User'
We will be adding similar lines there for each new model.
Organizing routes
As our webapp will probably have many routes, lets organize them into separated files and a folder
mkdir src/routes
vi src/routes/index.js
And lets move our routes from server.js into routes/index.js
import express from 'express'
const router = express.Router()
router.get('/', (req, res) => {
res.status(200).send('hello')
})
router.post('/', (req, res) => {
const message = `hello ${req.body.to}!`
res.json({ status: 'ok', message: message })
})
export default router
And update server.js removing the routes and replacing them with this lines
import index from './routes/index'
app.use('/', index)
Run the tests to make sure everything is working.
Now lets create a new file
vi src/routes/users.js
With content
import express from 'express'
import User from '../models/User'
const router = express.Router()
router.get('/', (req, res) => {
User.find().then(users => {
res.json(users)
})
})
export default router
And include it in server.js as well.
And create a new test test/integration/users_spec.js
import request from 'supertest'
import mongoose from 'mongoose'
import mockgoose from 'mockgoose'
import { expect } from 'chai'
import User from '../../src/models/User'
import appP from '../../src/server'
appP.then(app => {
describe('Users API', () => {
before(done => {
mockgoose(mongoose).then(() => {
done()
})
})
after(done => {
mockgoose.reset(done)
})
it('GET /users gives a list of users containing one user previously saved', (done) => {
const wayne = new User({
_id: '58220c92bdacd314f0c220c4',
name: 'Wayne',
created_at : new Date(2017, 10,10)
})
wayne.save().then(() => {
request(app)
.get('/users')
.expect(200, [ {
__v: 0,
_id: '58220c92bdacd314f0c220c4',
name: 'Wayne',
created_at : '2017-11-10T03:00:00.000Z'
} ], done)
})
})
})
})
Here we face the fact that the express app needs to be returned by server.js not before mongoose completly sets up (which is done by /config/mongoose). But the connect() method is asynchronous. That means that the test will start running with the app, but before mongoose connects causing errors.
So we need to refactor our server.js file so that it returns a Promise[app] that eventually once, all configs gets resolver will resolve to the app.
Change config/mongoose.js
// mongoose
import mongoose from 'mongoose'
mongoose.Promise = global.Promise
export * from '../models/all'
export const CONNECTION_STRING_URL = 'mongodb://localhost/myappdatabase'
export default function (app) {
return mongoose.connect(CONNECTION_STRING_URL)
}
Notice that it returns the connect return value (a promise)
Then the new version of server.js should use promises
import express from 'express'
import expressConfig from './config/express'
import mongoConfig from './config/express'
import index from './routes/index'
import users from './routes/users'
const app = express()
const setups = [expressConfig, mongoConfig]
const startUp = setups.map(setup => setup(app))
const appPromise = Promise.all(startUp).then(() => {
app.use('/', index)
app.use('/users', users)
return app
})
export default appPromise
And we need to refactor all of our tests that use the app, to be done after the promise
Basically the test needs to be surrounded by
import request from 'supertest'
import appP from '../../src/server'
appP.then(app => {
// ... test code here
})
And that's it. Run the tests.