确保在每个 Mocha 测试之前 Express 应用程序正在运行

20

我正在使用ExpressJS、NodeJS、Mongoose和Mocha开发REST API。

问题是我有一个app.coffee文件,它负责设置ExpressJS并连接到Mongoose。我的设置方法是首先连接Mongoose,如果成功,则启动ExpressJS App。

然而,当设置Mocha时,我需要确保在执行任何测试用例之前,包括所有异步代码在内,完全启动了app.coffee中存在的ExpressJS App。

为此,我创建了一个test_helper.coffee并将以下代码放入其中,但是即使app.coffee中的代码尚未完全执行完成,测试用例仍会开始执行,这实际上是有道理的:

before (done) ->
  require(__dirname + '/../src/app')
  done()

简而言之,我希望确保在执行任何测试用例之前,ExpressJS应用程序已经完全完成了设置。

我该如何做到这一点?


我曾经遇到过完全相同的问题。我重新设计了我的Express服务器,现在它不会等待数据库连接或任何类型的异步初始化。这对于REST服务器也是理想的,因为即使没有请求进入REST环境,也不需要连接到数据库。此外,每个调用并不一定需要所有模型。在我的情况下,我仅在必要时初始化数据库,而几乎每个调用都需要。 - Kamrul
@Kamrul:你能分享一些示例代码,展示你是如何做到的吗? - Haseeb Khan
6个回答

15

我来晚了,但我找到了在express应用中设置mocha测试套件的最佳方法是让我的app.js或server.js文件导出app对象,就像这样:

var mongoose = require('mongoose');
var express = require('express');
require('express-mongoose');

var env = process.env.NODE_ENV || 'development';
var config = require('./config/config')[env];

var models = require('./app/models');
var middleware = require('./app/middleware');
var routes = require('./app/routes');

var app = express();

app.set('port', process.env.PORT || config.port || 3000);
app.set('views', __dirname + '/app/views');
app.set('view engine', 'jade');

// database
mongoose.connect(config.db);

// middleware
middleware(app);

// Application routes
routes(app);

app.listen(app.get('port'));
console.log('Express server listening on port ' + app.get('port'));

// export app so we can test it
exports = module.exports = app;

确保您的配置文件中设置了不同的环境,如开发、测试、生产:

var path = require('path');
var rootPath = path.normalize(__dirname + '/..');

module.exports = {
  development: {
    db: 'mongodb://localhost/my_dev_db',
    port: 3000
  },
  test: {
    db: 'mongodb://localhost/my_test_db',
    port: 8888
  },
  production: {
    // ...
  }
}

然后在你的测试文件中,你可以继续引入你的应用程序,它将连接到正确的数据库和正确的端口:

var should = require('chai').should();
var request = require('supertest');
var mongoose = require('mongoose');

var app = require('../app');
var agent = request.agent(app);

var User = mongoose.model('User');

    // get users
    describe('GET /api/users', function() {
      it('returns users as JSON', function(done) {
        agent
        .get('/api/users')
        .expect(200)
        .expect('Content-Type', /json/)
        .end(function(err, res) {
          if (err) return done(err);
          res.body.should.have.property('users').and.be.instanceof(Array);
          done();
        });
      });
    });

最后,为了启动整个“怪兽”,您需要在package.json中添加以下内容(确保在devDependencies中有nodemon和mocha):
"scripts": {
    "start": "NODE_ENV=development ./node_modules/.bin/nodemon app.js",
    "test": "NODE_ENV=test ./node_modules/.bin/mocha --reporter spec test/**.js"
  }

现在,您可以使用npm test启动测试套件,使用npm start启动应用程序。

希望这有所帮助! 附注:我从这个令人惊叹的示例中学到了大部分内容: https://github.com/madhums/node-express-mongoose-demo


6
Supertest是否知道expressjs应用程序何时可以使用?当初始化HTTP谓词时,如何确保数据库已准备好使用? - raffomania
因为我从我的app.js文件中导出应用程序,并在每个测试文件的顶部要求它。 - tmaximini
4
那数据库呢?据我所知,你需要使用类似于mongoose.connection.once('open', callback)的东西,然后再添加你的路由。这意味着应用程序是真正异步初始化的。你如何处理这个? - raffomania
在上面的代码中,我在我的app.js文件中使用了mongoose.connect(config.db) - tmaximini
7
mongoose.connect会阻塞直到数据库准备就绪吗?如果是,那么事件('open'等)的作用是什么? - raffomania
显示剩余3条评论

3

我在使用jasmine/supertest测试我的express应用时遇到了同样的问题。我通过在应用程序准备就绪时发出信号并仅在此后运行测试来解决了这个问题。这是我的目录结构

- server.js
- spec
    - setup.spec.js
    - test.spec.js

server.js中,当您设置好服务器并准备运行测试时,请添加app.emit('started');。并确保导出您的应用程序!在setup.spec.js中,您可以观察事件是否被发出。

server.js

const express = require('express');
const app = express();
module.exports = app; // for testing

setTimeout(() => {
    app.listen(process.env.PORT || 3000);
});

setup.spec.js

const server = require('../index');

beforeAll(done => server.on('started', done));

test.spec.js

const request = require('supertest');
const server = require('../index');

describe('test', () => {
    it('test server works', done => {
        request(server).get('/test').expect(200);
     });
});

相同的想法同样适用于mocha。这里有一篇文章解释了如何在mocha中实现这一点。你只需要将beforeAll更改为before

是的,这对我也起作用了,只需在初始化/启动服务器后添加emit,然后在before函数中检查app.on它 - 迄今为止最好的答案。 - AKMorris
server.js 中调用 setTimeout() 是为了避免竞争条件,即在 'beforeAll()' 运行并准备好监听事件之前就发出了 'started' 事件,这是一种解决方法吗? - Dem Pilafian

1

可能有更简单的方法,但我使用Grunt自动化我的功能测试。有一个通过express和mocha插件来实现你的目标。我的gruntfile:

'use strict';

module.exports = function (grunt) {
grunt.initConfig({
    express: {
        options: {}
      , test: {
            options: {
                script: './app.js'
            }
        }
    }
  , simplemocha: {
        options: {
            globals: ['should']
          , timeout: 8000
          , ignoreLeaks: false
          , ui: 'bdd'
          , reporter: 'tap'
        }
      , all: { src: ['tests/*.test.js'] }
    }
})

grunt.loadNpmTasks('grunt-express-server')
grunt.loadNpmTasks('grunt-simple-mocha')

grunt.registerTask('default', ['express:test', 'simplemocha', 'express:test:stop'])
}

奖励: 将'grunt'添加为git的pre-commit hook。这样,您就无法提交未通过所有测试的内容。


0

app.listen 方法接受一个回调函数,当一切准备就绪时运行。因此,您需要能够在那里传递 done 回调函数。类似于:

before (done) ->
  var app = require(__dirname + '/../src/app')
  app.listen(3000, done)

0

测试应用程序时,您无需侦听端口。您可以使用supertest测试库与您的应用程序一起使用,这样就可以了。

但是,您可能需要连接到数据库或Redis客户端。您可以在应用程序的configure方法中执行此操作。由于节点缓存模块,因此您可以在不重新连接的情况下在不同的测试模块中要求应用程序模块。


问题在于我的应用程序设置是异步的,包括当我的 Express 路由准备就绪时,因此 supertest 每次尝试访问该路由时都会失败,因为它启动得太早了。 - writofmandamus
2
你不需要监听端口。你可以允许 supertest 为你处理 - dvlsg
1
“你不需要监听端口来测试你的应用程序。” “哦,这很新奇。那么它是如何工作的呢?” - j3141592653589793238

0

基本上你需要做两件事情。

  1. 确保数据库在服务器监听之前连接成功。
  2. 确保应用程序在运行测试之前已经启动。

这个框架已经有一种处理这个问题的方法,就是使用事件。

将以下代码添加到你的数据库连接中,它会指示mongoose在连接完成后发出'ready'事件。

mongoose.connection.once('open', function() { 
   // All OK - fire (emit) a ready event. 
   console.log('Connected to MongoDB');
   app.emit('ready'); 
});

然后指示Express在监听之前等待“ready”。完成后,它会发出:“appStarted”。

app.on('ready', function() { 
    app.listen(PORT, function(){ 
        console.log("Server listing on port:",PORT); 
        app.emit("appStarted");
    }); 
}); 

这是所有良好的实践,Mocha除外,以确保您的服务器顺利启动。要完成before中的Mocha集成(我喜欢使用Promises):

before(function() {        
    return new Promise((resolve,reject) => {
        app.on("appStarted", function(){
            return resolve();
        }); 
    });
});

似乎这个方法可行是因为'ready'事件有足够的延迟,以至于Mocha的before()函数已经运行并等待'appStarted'事件。如果数据库连接非常快,可能会出现竞争条件,即'appStarted'被过早触发,而Mocha无法看到它。 - Dem Pilafian

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