如何使用Mocha测试我的Express应用程序?

67

我刚刚为我的Express应用程序添加了shouldjs和mocha进行测试,但我想知道如何测试我的应用程序。我希望像这样做:

app = require '../app'
routes = require '../src/routes'

describe 'routes', ->
  describe '#show_create_user_screen', ->
    it 'should be a function', ->
      routes.show_create_user_screen.should.be.a.function
    it 'should return something cool', ->
      routes.show_create_user_screen().should.be.an.object

当然,那个测试套件中的最后一个测试告诉我res.render函数(在show_create_user_screen内部调用)未定义,很可能是因为服务器没有运行并且配置尚未完成。所以我想知道其他人是如何设置他们的测试的?


只是想补充一下,上面的例子之所以被发布,是因为它简短而且简明。通常情况下,在调用我的路由函数之后,我会测试给定的 req/res 对象上的适当函数或值是否已设置/调用。对此,下面的答案是足够的。我们不应该测试路由器功能,这是 Web 框架的工作。 - Robin Heggelund Hansen
5个回答

66

我在connect.js测试套件中找到了一种替代方案。

他们使用supertest来测试连接应用程序,而无需将服务器绑定到任何端口或使用模拟。

以下是connect的静态中间件测试套件的摘录(使用mocha作为测试运行程序和supertest进行断言)。

var connect = require('connect');

var app = connect();
app.use(connect.static(staticDirPath));

describe('connect.static()', function(){
  it('should serve static files', function(done){
    app.request()
    .get('/todo.txt')
    .expect('contents', done);
  })
});

这同样适用于Express应用程序


我只能接受一个答案,否则这个也会被接受 =) - Robin Heggelund Hansen
1
app.request在最新的express/connect中对我不起作用,因此我已更新此答案以匹配https://github.com/visionmedia/supertest上的用法。 - Aaron
关于 supertest 的条款看起来有误导性。在 connect 代码 中似乎没有提到它。不管怎样,Alexandru 的答案比其他人的更好。 - yanychar
这里是使用 supertestconnect 部分:https://github.com/senchalabs/connect/blob/2fe55712b6d204dd8a9132d29685f0c4e6be48db/test/server.js#L5 - Félix Saparelli

33

首先,尽管测试你的路由代码是你可能想要或不想要做的事情,但通常来说,尝试将有趣的业务逻辑分离为纯JavaScript代码(类或函数),这些代码与express或您正在使用的任何框架解耦,并使用原生mocha测试进行测试。一旦你实现了这一点,如果你想真正测试在mocha中配置的路由,你需要传递模拟req, res参数到你的中间件函数中,以模仿express/connect和你的中间件之间的接口。

对于一个简单的情况,你可以创建一个模拟的res对象,带有一个类似下面的render函数。

describe 'routes', ->
  describe '#show_create_user_screen', ->
    it 'should be a function', ->
      routes.show_create_user_screen.should.be.a.function
    it 'should return something cool', ->
      mockReq = null
      mockRes =
        render: (viewName) ->
          viewName.should.exist
          viewName.should.match /createuser/

      routes.show_create_user_screen(mockReq, mockRes).should.be.an.object

另外仅供参考,中间件函数不需要返回任何特定值,重点是它们如何使用req、res、next参数在测试中进行处理。

以下是你在评论中要求的一些JavaScript代码。

describe('routes', function() {
    describe('#show_create_user_screen', function() {
      it('should be a function', function() {
        routes.show_create_user_screen.should.be.a["function"];
      });
      it('should return something cool', function() {
        var mockReq = null;
        var mockRes = {
          render: function(viewName) {
            viewName.should.exist;
            viewName.should.match(/createuser/);
          }
        };
        routes.show_create_user_screen(mockReq, mockRes);
      });
    });
  });

1
模拟测试无法保护您免受所使用模块的 API 更改的影响。例如,如果 Express 更新并更改了 render 的名称,则您将无法得到保护。理想情况下,您也应该对此进行测试,但有时集成和单元测试可以同时测试大量代码,这可能是好事或坏事,具体取决于您的看法。编辑:虽然我真的很喜欢这种模拟方法,但它确实非常轻量级。 - timoxley
64
请始终附上编译后的JS文件,因为一些人不习惯阅读CoffeeScript。 - Julius F
这不是在测试实现细节吗?实际上,您想要测试返回时'response'对象包含什么 - 如果将来不使用'render'方法进行此操作,例如在重构期间,您的测试将失败并且无法显示您的重构代码是否有效,因为您必须重写测试?只是一个想法!否则,这是一种巧妙地模拟响应对象的方法。 - SimonB
Supertest是更多端到端测试的另一种方法。两者都有其用途。 - Peter Lyons

24

你可以尝试使用 SuperTest,这样服务器的启动和关闭都会被处理:

var request = require('supertest')
  , app     = require('./anExpressServer').app
  , assert  = require("assert");

describe('POST /', function(){
  it('should fail bad img_uri', function(done){
    request(app)
        .post('/')
        .send({
            'img_uri' : 'foobar'
        })
        .expect(500)
        .end(function(err, res){
            done();
        })
  })
});

SuperTest 对我来说至少在十几个项目中都非常好用。胜利! - deepelement
1
我想知道supertestchaihttp之间有什么区别? - 4Z4T4R

7

mocha内置了before、beforeEach、after和afterEach用于BDD测试。 在这种情况下,您应该在describe调用中使用before。

describe 'routes' ->
  before (done) ->
    app.listen(3000)
    app.on('connection', done)

6

我发现设置一个TestServer类作为辅助以及一个帮助http客户端,并实际向真正的http服务器发送请求,是最容易的。虽然有时候你可能想要模拟和存根这些内容。

// Test file
var http = require('the/below/code');

describe('my_controller', function() {
    var server;

    before(function() {
        var router = require('path/to/some/router');
        server = http.server.create(router);
        server.start();
    });

    after(function() {
        server.stop();
    });

    describe("GET /foo", function() {
        it('returns something', function(done) {
            http.client.get('/foo', function(err, res) {
                // assertions
                done();
            });
        });
    });
});


// Test helper file
var express    = require('express');
var http       = require('http');

// These could be args passed into TestServer, or settings from somewhere.
var TEST_HOST  = 'localhost';
var TEST_PORT  = 9876;

function TestServer(args) {
    var self = this;
    var express = require('express');
    self.router = args.router;
    self.server = express.createServer();
    self.server.use(express.bodyParser());
    self.server.use(self.router);
}

TestServer.prototype.start = function() {
    var self = this;
    if (self.server) {
        self.server.listen(TEST_PORT, TEST_HOST);
    } else {
        throw new Error('Server not found');
    }
};

TestServer.prototype.stop = function() {
    var self = this;
    self.server.close();
};

// you would likely want this in another file, and include similar 
// functions for post, put, delete, etc.
function http_get(host, port, url, cb) {
    var options = {
        host: host,
        port: port,
        path: url,
        method: 'GET'
    };
    var ret = false;
    var req = http.request(options, function(res) {
        var buffer = '';
        res.on('data', function(data) {
            buffer += data;
        });
        res.on('end',function(){
            cb(null,buffer);
        });
    });
    req.end();
    req.on('error', function(e) {
        if (!ret) {
            cb(e, null);
        }
    });
}

var client = {
    get: function(url, cb) {
        http_get(TEST_HOST, TEST_PORT, url, cb);
    }
};

var http = {
    server: {
        create: function(router) {
            return new TestServer({router: router});
        }
    },

    client: client
};
module.exports = http;

刚意识到我错过了你问题的重点,但也许这会有所帮助。个人而言,我不会测试路由器本身的功能。我只是通过 HTTP 请求进行测试,以确保服务器基本上执行它应该执行的操作,然后单独测试所有业务逻辑,因为它们都在控制器之外的文件中。 - Bryan Donovan
你已经得到了对 path/to/some/router 的引用,查看该文件的内容会很有帮助。 - ryonlife

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