如何使用Express测试一个单元的路由?


99

我正在学习Node.js,并且一直在玩Express。真的很像框架;但是,我很难弄清楚如何为路由编写单元/集成测试。

能够对简单的模块进行单元测试很容易,并且已经通过Mocha完成了;但是,由于我传递的响应对象未保留值,因此使用Express进行的单元测试失败。

待测路由功能(routes / index.js):

exports.index = function(req, res){
  res.render('index', { title: 'Express' })
};

单元测试模块:

var should = require("should")
    , routes = require("../routes");

var request = {};
var response = {
    viewName: ""
    , data : {}
    , render: function(view, viewData) {
        viewName = view;
        data = viewData;
    }
};

describe("Routing", function(){
    describe("Default Route", function(){
        it("should provide the a title and the index view name", function(){
        routes.index(request, response);
        response.viewName.should.equal("index");
        });

    });
});

当我运行它时,它因“错误:检测到全局泄漏:viewName,数据”而失败。

  1. 我在哪里出错,以便可以正常工作?

  2. 有没有更好的方法可以在此级别对代码进行单元测试?

更新 1.由于我最初忘记了“ it()”,因此更正了代码段。

Answers:


21

更改您的响应对象:

var response = {
    viewName: ""
    , data : {}
    , render: function(view, viewData) {
        this.viewName = view;
        this.data = viewData;
    }
};

它将起作用。


2
这是对请求处理程序而不是路由进行单元测试。
杰森·塞布林

43

正如其他人在评论中建议的那样,测试Express控制器的规范方法似乎是通过supertest

测试示例如下所示:

describe('GET /users', function(){
  it('respond with json', function(done){
    request(app)
      .get('/users')
      .set('Accept', 'application/json')
      .expect(200)
      .end(function(err, res){
        if (err) return done(err);
        done()
      });
  })
});

优势:您可以一次测试整个堆栈。

缺点:感觉和行为有点像集成测试。


1
我喜欢这个,但是有一种方法可以断言viewName(就像在原始问题中一样)-还是我们必须对响应的内容断言?
亚历克斯

19
我同意您的缺点,这不是单元测试。这依赖于所有单元的集成来测试应用程序的URL。
路加H

10
我认为说“路线”确实是一个合法的说法,integration也许应该将测试路线留给集成测试。我的意思是,匹配其定义的回调的路由功能大概已经由express.js进行了测试;理想情况下,任何用于获取路径最终结果的内部逻辑都应在其外部进行模块化,并且这些模块应进行单元测试。他们的互动,即路线,应该经过集成测试。你同意吗?
Aditya MP

1
这是端到端测试。毫无疑问。
kgpdeveloper

23

我得出的结论是,真正对快速表达的应用程序进行单元测试的唯一方法是在请求处理程序和您的核心逻辑之间保持大量的分离。

因此,您的应用程序逻辑应该在可以进行单独require测试和单元测试的单独模块中,并且对Express Request和Response类的依赖性最小。

然后,在请求处理程序中,您需要调用核心逻辑类的适当方法。

完成当前应用程序的重组后,我将举一个例子!

我猜是这样吗?(随时提出要点或评论,我仍在探索中)。

编辑

这是一个小例子,内联。有关更多详细示例,请参见要点

/// usercontroller.js
var UserController = {
   _database: null,
   setDatabase: function(db) { this._database = db; },

   findUserByEmail: function(email, callback) {
       this._database.collection('usercollection').findOne({ email: email }, callback);
   }
};

module.exports = UserController;

/// routes.js

/* GET user by email */
router.get('/:email', function(req, res) {
    var UserController = require('./usercontroller');
    UserController.setDB(databaseHandleFromSomewhere);
    UserController.findUserByEmail(req.params.email, function(err, result) {
        if (err) throw err;
        res.json(result);
    });
});

3
我认为,这是最好的使用方式。跨语言的许多Web框架都使用控制器模式将业务逻辑与实际的HTTP响应形成功能分开。这样,您可以只测试逻辑而不是整个http响应过程,这是框架的开发人员应该自己进行测试的事情。在此模式下可以测试的其他内容是简单的中间件,一些验证功能和其他业务服务。数据库连接测试是一种完全不同的测试类型
OzzyTheGiant19,19年

1
确实,这里的许多答案确实与集成/功能测试有关。
卢克H

19

使用express测试HTTP的最简单方法是窃取TJ的http帮助器

个人使用他的助手

it("should do something", function (done) {
    request(app())
    .get('/session/new')
    .expect('GET', done)
})

如果要专门测试路由对象,则传递正确的模拟

describe("Default Route", function(){
    it("should provide the a title and the index view name", function(done){
        routes.index({}, {
            render: function (viewName) {
                viewName.should.equal("index")
                done()
            }
        })
    })
})

5
您可以修复“助手”链接吗?
尼古拉斯·默里

16
似乎HTTP单元测试的最新方法是使用Visionmedia的supertest。TJ的http助手似乎也已经发展成为超级测试。
AkseliPalén'13

2
可以在这里
Brandon

@Raynos您能否在示例中解释如何获取请求和应用程序?
jmcollin92

9
可悲的是,这是集成测试而不是单元测试。
路加H

8

如果使用express 4进行单元测试,请注意gjohnson的以下示例:

var express = require('express');
var request = require('supertest');
var app = express();
var router = express.Router();
router.get('/user', function(req, res){
  res.send(200, { name: 'tobi' });
});
app.use(router);
request(app)
  .get('/user')
  .expect('Content-Type', /json/)
  .expect('Content-Length', '15')
  .expect(200)
  .end(function(err, res){
    if (err) throw err;
  });

1

我也想知道,但是专门针对单元测试而不是集成测试。这就是我现在正在做的

test('/api base path', function onTest(t) {
  t.plan(1);

  var path = routerObj.path;

  t.equals(path, '/api');
});


test('Subrouters loaded', function onTest(t) {
  t.plan(1);

  var router = routerObj.router;

  t.equals(router.stack.length, 5);
});

routerObj在哪里{router: expressRouter, path: '/api'}。然后,我用加载子路由器,var loginRouterInfo = require('./login')(express.Router({mergeParams: true}));然后express应用程序调用init函数,将init路由器作为参数。然后,initRouter调用router.use(loginRouterInfo.path, loginRouterInfo.router);以安装子路由器。

可以使用以下方法测试子路由器:

var test = require('tape');
var routerInit = require('../login');
var express = require('express');
var routerObj = routerInit(express.Router());

test('/login base path', function onTest(t) {
  t.plan(1);

  var path = routerObj.path;

  t.equals(path, '/login');
});


test('GET /', function onTest(t) {
  t.plan(2);

  var route = routerObj.router.stack[0].route;

  var routeGetMethod = route.methods.get;
  t.equals(routeGetMethod, true);

  var routePath = route.path;
  t.equals(routePath, '/');
});

3
这看起来真的很有趣。您是否还有更多遗失物品的例子来说明这一切如何融合在一起?
cjbarth

1

为了实现单元测试而不是集成测试,我模拟了请求处理程序的响应对象。

/* app.js */
import endpointHandler from './endpointHandler';
// ...
app.post('/endpoint', endpointHandler);
// ...

/* endpointHandler.js */
const endpointHandler = (req, res) => {
  try {
    const { username, location } = req.body;

    if (!(username && location)) {
      throw ({ status: 400, message: 'Missing parameters' });
    }

    res.status(200).json({
      location,
      user,
      message: 'Thanks for sharing your location with me.',
    });
  } catch (error) {
    console.error(error);
    res.status(error.status).send(error.message);
  }
};

export default endpointHandler;

/* response.mock.js */
import { EventEmitter } from 'events';

class Response extends EventEmitter {
  private resStatus;

  json(response, status) {
    this.send(response, status);
  }

  send(response, status) {
    this.emit('response', {
      response,
      status: this.resStatus || status,
    });
  }

  status(status) {
    this.resStatus = status;
    return this;
  }
}

export default Response;

/* endpointHandler.test.js */
import Response from './response.mock';
import endpointHandler from './endpointHander';

describe('endpoint handler test suite', () => {
  it('should fail on empty body', (done) => {
    const res = new Response();

    res.on('response', (response) => {
      expect(response.status).toBe(400);
      done();
    });

    endpointHandler({ body: {} }, res);
  });
});

然后,要实现集成测试,您可以模拟您的endpointHandler并使用supertest调用端点。


0

在我的情况下,我唯一要测试的是是否已调用正确的处理程序。我想使用supertest来简化向路由中间件发出请求的过程。我正在使用Typescript a,这是对我有用的解决方案

// ProductController.ts

import { Request, Response } from "express";

class ProductController {
  getAll(req: Request, res: Response): void {
    console.log("this has not been implemented yet");
  }
}
export default ProductController

路线

// routes.ts
import ProductController  from "./ProductController"

const app = express();
const productController = new ProductController();
app.get("/product", productController.getAll);

测试

// routes.test.ts

import request from "supertest";
import { Request, Response } from "express";

const mockGetAll = jest
  .fn()
  .mockImplementation((req: Request, res: Response) => {
    res.send({ value: "Hello visitor from the future" });
  });

jest.doMock("./ProductController", () => {
  return jest.fn().mockImplementation(() => {
    return {
      getAll: mockGetAll,

    };
  });
});

import app from "./routes";

describe("Routes", () => {
  beforeEach(() => {
    mockGetAll.mockImplementation((req: Request, res: Response) => {
      res.send({ value: "You can also change the implementation" });
    });
  });

  it("GET /product integration test", async () => {
    const result = await request(app).get("/product");

    expect(mockGetAll).toHaveBeenCalledTimes(1);

  });



  it("GET an undefined route should return status 404", async () => {
    const response = await request(app).get("/random");
    expect(response.status).toBe(404);
  });
});

我有一些问题可以使模拟工作。但是使用jest.doMock和示例中显示的特定顺序可以使其正常工作。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.