我已阅读Stack Overflow上的以下帖子:
Mocking/stubbing Mongoose model save method
我也研究过mockgoose,但我更喜欢使用testdouble或sinon来存根/模拟我的数据库调用。
找到的信息here可能是最接近我想做的事情。但我无法完全理解它。我认为,不同之处在于我试图在我的api中测试路线,而不是直接测试Mongoose模型。这是我的代码:
server.ts
for file in os.listdir (testdir):
lc = 0
filename = '%s/%s' % (testdir, file)
if file.endswith ('.txt') and os.path.isfile (filename):
with open (filename) as f:
for line in f:
lc += 1
print ('%s has %s lines inside.' % (filename, lc))
else:
print ('No such file - %s' % filename)
/server/db.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/models/user.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/routes/api.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/user.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.spec.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
});
});
});
我使用supertest伪造请求并使用Jasmine作为测试框架和跑步者。
我的问题:为了让这个测试绕过调用数据库而不是使用存根或模拟,我需要在spec文件中更改什么?
答案 0 :(得分:4)
我相信您正在寻找的答案可以在此视频中找到: Unit Testing Express Middleware / TDD with Express and Mocha
我已经决定遵循它的指示,到目前为止一直很棒。问题是在路由和中间件之间拆分路由,这样您就可以在不调用或启动服务器的情况下测试业务逻辑。使用node-mocks-http可以模拟请求和响应参数。
要模拟我的模型调用,我使用sinon来存储应该访问数据库的get,list和stuff等方法。对于您的情况,相同的视频将提供使用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)
})
})
})
在这个例子中,我想模拟一个未找到的错误,但你可以在任何返回的地方存根,你可能需要适合你的中间件测试。
答案 1 :(得分:1)
Jasmine使用间谍使嘲弄事情变得非常简单。首先要做的是使用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();
});
});
答案 2 :(得分:0)
使用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
}));
注意:
sinon.test
语法,所以您不必担心重置存根。