不用访问数据库进行单元测试Express/Mongoose应用程序路由

5

我已经阅读了 Stack Overflow 上的以下帖子:

Mongoose 单元测试

模拟/存根 Mongoose 模型保存方法

我还研究了 mockgoose,但我更喜欢使用 testdouble 或 sinon 来存根/模拟我的数据库调用。

在此处找到的信息可能最接近我想要做的事情。但我无法完全理解。我认为区别在于我正在尝试测试我的 API 中的路由,而不是直接测试 Mongoose 模型。这是我的代码:

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作为测试框架和运行器。
我的问题是:为了使这个测试绕过对数据库的调用并使用存根或模拟,我需要在规范文件中做哪些修改?

是“单元测试”还是“集成测试”? - Fazal Rasel
嗯,那是个好问题。我想我正在测试不止一个函数,所以也许应该被视为集成测试。我试图做的是测试router.post('/')是否按照预期执行,即获取请求体并调用数据库创建新用户,然后发送正确的响应。但我不想让测试实际调用数据库。我想要伪造那部分。 - snowfrogdev
3个回答

4
我相信你要找的答案可以在这个视频中找到:Unit Testing Express Middleware / TDD with Express and Mocha
我决定按照视频中的指示进行操作,目前效果很好。关键是将路由分为路由和中间件,这样就可以在不调用或启动服务器的情况下测试业务逻辑。使用node-mocks-http,您可以模拟请求和响应参数。
为了模拟我的模型调用,我正在使用sinon来存根方法,例如get、list和应该命中数据库的其他方法。对于您的情况,同一视频将提供使用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来stub像get、list等应该与数据库交互的方法,模拟你的模型调用的方式? - snowfrogdev

3

Jasmine使用spy很容易模拟对象。首先,应该使用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();
        });
});

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
}));

注意事项:

  1. 由于我们使用的是 sinon.test 语法,您不必担心重置存根。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接