unit testingExpress / Mongoose的应用程序路线,而不击中数据库

我已经阅读了堆栈溢出以下职位:

unit testing与mongoose

嘲笑/ st Mong模式保存方法

我也看了mockgoose,但我宁愿使用testdouble或sinon存根/模拟我的数据库调用。

在这里find的信息可能是最接近我想要做的。 但是我无法把头围住。 我认为,区别在于我试图在api中testing路由,而不是直接testingMongoose模型。 这是我的代码:

server.ts

import * as express from 'express'; const app = express() import { createServer } from 'http'; const server = createServer(app); import * as ioModule from 'socket.io'; const io = ioModule(server); import * as path from 'path'; import * as bodyParser from 'body-parser'; import * as helmet from 'helmet'; import * as compression from 'compression'; import * as morgan from 'morgan'; // Database connection import './server/db'; // Get our API routes and socket handler import { api } from './server/routes/api' import { socketHandler } from './server/socket/socket'; // Helmet security middleware app.use(helmet()); // Gzip compression middleware app.use(compression()); // Morgan logging middleware app.use(morgan('common')); // Parsers for POST data app.use(bodyParser.json()); app.use(bodyParser.urlencoded({ extended: false })); // Point static path to dist app.use(express.static(path.join(__dirname, 'dist'))); // Set our api routes app.use('/api', api); // Catch all other routes and return the index file app.get('*', (req: any, res: any) => { res.sendFile(path.join(__dirname, 'dist/index.html')); }); /** * Get port from environment and store in Express. */ const port = process.env.PORT || '3000'; app.set('port', port); /** * Listen on provided port, on all network interfaces. */ server.listen(port, () => console.log(`API running on localhost:${port}`)); io.on('connection', socketHandler); export { server }; 

/server/db.ts

 import * as mongoose from 'mongoose'; // Enter database URL and delete this comment const devDbUrl = 'mongodb://localhost:27017/book-trade'; const prodDbUrl = process.env.MONGOLAB_URI; const dbUrl = devDbUrl || prodDbUrl; mongoose.connect(dbUrl); (<any>mongoose).Promise = global.Promise; mongoose.connection.on('connected', () => { console.log('Mongoose connected to ' + dbUrl); }); mongoose.connection.on('disconnected', () => { console.log('Mongoose disconnected'); }); mongoose.connection.on('error', (err: any) => { console.log('Mongoose connection error' + err); }); process.on('SIGINT', () => { mongoose.connection.close(() => { console.log('Mongoose disconnected through app termination (SIGINT)'); process.exit(0); }); }); process.on('SIGTERM', () => { mongoose.connection.close(() => { console.log('Mongoose disconnected through app termination (SIGTERM)'); process.exit(0); }); }); process.once('SIGUSR2', () => { mongoose.connection.close(() => { console.log('Mongoose disconnected through app termination (SIGUSR2)'); process.kill(process.pid, 'SIGUSR2'); }); }); 

/server/models/user.ts

 import * as mongoose from 'mongoose'; const Schema = mongoose.Schema; const mongooseUniqueValidator = require('mongoose-unique-validator'); export interface IUser extends mongoose.Document { firstName: string, lastName: string, city: string, state: string, password: string, email: string, books: Array<{ book: any, onLoan: boolean, loanedTo: any }> } const schema = new Schema({ firstName: { type: String, required: true }, lastName: { type: String, required: true }, city: { type: String, required: true }, state: { type: String, required: true }, password: { type: String, required: true }, email: { type: String, required: true, unique: true }, books: [{ book: { type: Schema.Types.ObjectId, ref: 'Book', required: true}, onLoan: { type: Boolean, required: true }, loanedTo: { type: Schema.Types.ObjectId, ref: 'User'} }] }); schema.plugin(mongooseUniqueValidator); export const User = mongoose.model<IUser>('User', schema); 

/server/routes/api.ts

 import * as express from 'express'; const router = express.Router(); import { userRoutes } from './user'; /* GET api listing. */ router.use('/user', userRoutes); export { router as api }; 

/server/routes/user.ts

 import * as express from 'express'; const router = express.Router(); import * as bcrypt from 'bcryptjs'; import { User } from '../models/user'; router.post('/', function (req, res, next) { bcrypt.hash(req.body.password, 10) .then((hash) => { const user = new User({ firstName: req.body.firstName, lastName: req.body.lastName, city: req.body.city, state: req.body.state, password: hash, email: req.body.email }); return user.save(); }) .then((user) => { res.status(201).json({ message: 'User created', obj: user }); }) .catch((error) => { res.status(500).json({ title: 'An error occured', error: error }); }); }); 

/server/routes/user.spec.ts

 import * as request from 'supertest'; import * as td from 'testdouble'; import { server } from '../../server'; import { finishTest } from '../../spec/helpers/suptertest'; describe('user route', function () { let app: any; beforeEach(function () { app = server; }); afterEach(function (done) { app.close(done); }); it('creates a user /', (done) => { //make request request(app) .post('/api/user') .send({ firstName: 'Philippe', lastName: 'Vaillancourt', city: 'Laval', state: 'Qc', password: 'test', email: 'test@test.com', }) .expect(201, finishTest(done)); }); }); 

我使用supertest伪造请求,并使用Jasmine作为testing框架和跑步者。

我的问题:我需要在我的spec文件中更改什么,以便让此testing绕过对数据库的调用,而使用存根或模拟?

茉莉花使用间谍嘲笑事情很简单。 首先要做的是使用Model.create而不是new关键字,然后你可以窥探模型方法并覆盖它们的行为来返回一个模拟。

 // Import model so we can apply spies to it... import {User} from '../models/user'; // Example mock for document creation... it('creates a user', (done) => { let user = { firstName: 'Philippe', lastName: 'Vaillancourt', city: 'Laval', state: 'Qc', password: 'test', email: 'test@test.com' }; spyOn(User, 'create').and.returnValue(Promise.resolve(user)); const request = { firstName: 'Philippe', lastName: 'Vaillancourt', city: 'Laval', state: 'Qc', password: 'test', email: 'test@test.com' }; request(app) .post('/api/user') .send(request) .expect(201) .end((err) => { expect(User.create).toHaveBeenCalledWith(request); if (err) { return done(err); } return done(); }); }); // Example mock for document querying... it('finds a user', (done) => { let user = { firstName: 'Philippe', lastName: 'Vaillancourt', city: 'Laval', state: 'Qc', password: 'test', email: 'test@test.com' }; let query = jasmine.createSpyObj('Query', ['lean', 'exec']); query.lean.and.returnValue(query); query.exec.and.returnValue(Promise.resolve(user)); spyOn(User, 'findOne').and.returnValue(query); request(app) .get('/api/user/Vaillancourt') .expect(200) .end((err) => { expect(User.findOne).toHaveBeenCalledWith({lastName: 'Vaillancourt'}); expect(query.lean).toHaveBeenCalled(); expect(query.exec).toHaveBeenCalled(); if (err) { return done(err); } return done(); }); }); 

我相信你正在寻找的答案可以在这个video中find: unit testing快递中间件/ TDD与Express和摩卡

我已经决定遵循它的指示,迄今为止已经很棒了。 问题是在路由和中间件之间拆分路由,这样就可以在不调用或启动服务器的情况下testing业务逻辑。 使用node-mocks-http可以模拟请求和响应参数。

为了嘲笑我的模型调用,我正在使用sinon来存储get,list和应该碰到数据库的东西。 对于你的情况,同一个video将提供一个使用mockgoose的例子。

一个简单的例子可能是:

 /* global beforeEach afterEach describe it */ const chai = require('chai') const chaiAsPromised = require('chai-as-promised') const sinon = require('sinon') const httpMocks = require('node-mocks-http') const NotFoundError = require('../../app/errors/not_found.error') const QuestionModel = require('../../app/models/question.model') const QuestionAdminMiddleware = require('../../app/middlewares/question.admin.middleware') chai.use(chaiAsPromised) const expect = chai.expect let req let res beforeEach(() => { req = httpMocks.createRequest() res = httpMocks.createResponse() sinon.stub(QuestionModel, 'get').callsFake(() => { return new Promise((resolve) => { resolve(null) }) }) }) afterEach(() => { QuestionModel.list.restore() QuestionModel.get.restore() }) describe('Question Middleware', () => { describe('Admin Actions', () => { it('should throw not found from showAction', () => { return expect(QuestionAdminMiddleware.showAction(req, res)) .to.be.rejectedWith(NotFoundError) }) }) }) 

使用sinon.js来存储模型。

 var sinon = require('sinon'); var User = require('../../application/models/User'); it('should fetch a user', sinon.test(function(done) { var stub = this.stub(User, 'findOne', function(search, fields, cb) { cb(null, { _id: 'someMongoId', name: 'someName' }); }); // mocking an instance method // the `yields` method calls the supplied callback with the arguments passed to it this.stub(User.prototype, 'save').yields(null, { _id: 'someMongoId', name: 'someName' }); // make an http call to the route that uses the User model. // the findOne method in that route will now return the stubbed result // without making a call to the database // call `done();` when you are finished testing })); 

笔记:

  1. 因为我们使用的是sinon.test语法,所以您不必担心重置存根。