【问题标题】:How does one unit test routes with Express?一个单元如何使用 Express 测试路由?
【发布时间】:2012-03-01 14:24:46
【问题描述】:

我正在学习 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. 更正代码 sn-p,因为我最初忘记了“it()”。

【问题讨论】:

    标签: node.js express


    【解决方案1】:

    正如其他人在 cmets 中所推荐的那样,测试 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()
          });
      })
    });
    

    好处:您可以一次性测试整个堆栈。

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

    【讨论】:

    • 我喜欢这个,但有没有办法断言 viewName (如在原始问题中) - 还是我们必须断言响应的内容?
    • 我同意你的缺点,这不是单元测试。这依赖于所有单元的集成来测试应用程序的 url。
    • 我认为说“路由”实际上是integration 是合法的,也许测试路由应该留给集成测试。我的意思是,路由与其定义的回调匹配的功能可能已经由 express.js 测试过;任何用于获得路由最终结果的内部逻辑,理想情况下都应该在其外部进行模块化,并且这些模块应该进行单元测试。他们的交互,即路线,应该进行集成测试。你同意吗?
    • 这是端到端的测试。毫无疑问。
    【解决方案2】:

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

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

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

    一旦我完成了对当前应用程序的重构,我将举一个例子!

    我猜像this? 之类的东西(请随意提出要点或评论,我仍在探索这个)。

    编辑

    这是一个小例子,内联。有关更详细的示例,请参阅 the gist

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

    【讨论】:

    • 在我看来,这是最好的模式。许多跨语言的 Web 框架使用控制器模式将业务逻辑与实际的 http 响应形成功能分开。这样,您可以只测试逻辑而不是整个http响应过程,这是框架开发人员应该自己测试的东西。可以在此模式中测试的其他内容是简单的中间件、一些验证功能和其他业务服务。数据库连接测试是一种完全不同类型的测试
    • 确实,这里的很多答案都与集成/功能测试有关。
    • 这是正确的答案。你应该专注于测试你的逻辑,而不是 Express。
    • 我真的很喜欢这种方法!如果可以,我会使用 OpenAPI 生成器 (github.com/OpenAPITools/openapi-generator),它从 OpenAPI 文档创建应用程序框架。生成器的输出是路由、控制器和服务。路由不需要测试,控制器是最小的,通常应该不理会,真正的业务逻辑在服务中 - 编写适当的代码和测试。
    【解决方案3】:

    更改您的响应对象:

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

    它会起作用的。

    【讨论】:

    • 这是对请求处理程序的单元测试,而不是路由。
    【解决方案4】:

    用 express 测试 HTTP 最简单的方法是窃取TJ's http helper

    personally use his helper

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

    【讨论】:

    • 你能修复“帮助”链接吗?
    • 似乎最新的 HTTP 单元测试方法是使用 Visionmedia 的supertest。 TJ 的 http 助手似乎也已经进化为超级测试。
    • github上的supertest可以找到here
    • @Raynos 你能解释一下你是如何在你的例子中获得请求和应用程序的吗?
    • 遗憾的是这是集成测试而不是单元测试。
    【解决方案5】:

    如果使用 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;
      });
    

    【讨论】:

      【解决方案6】:

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

      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})); 加载子路由器,然后快速应用程序调用一个初始化函数,将快速路由器作为参数。然后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, '/');
      });
      

      【讨论】:

      • 这看起来很有趣。你有更多缺失部分的例子来展示这一切是如何结合在一起的吗?
      【解决方案7】:

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

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

      然后,为了实现集成测试,您可以模拟您的端点处理程序并使用supertest 调用端点。

      【讨论】:

        【解决方案8】:

        就我而言,我唯一想测试的是是否调用了正确的处理程序。我想使用 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 和您在示例中看到的特定顺序使其工作。

        【讨论】:

          【解决方案9】:

          如果您想避免超测试,您可以简单地模拟请求和响应,并像任何其他异步函数一样对其进行测试。

            let handlerStatus = 0;
            let handlerResponse: any = {}; // can replace any with the strong type
          
            const req: Request = {
              //  inject here the request details
              headers: { authorization: 'XXXXX' },
            } as Request;
          
            const res = {
              json(body: any) { // can replace any with the strong type
                handlerResponse = body;
              },
              status(status: number) {
                handlerStatus = status;
                return this;
              },
            } ;
          
            await callYourHanlderFunction(req, res as Response);
          
            expect(handlerStatus).toBe(200);
            expect(handlerResponse).toEqual(correctResponse);
          

          【讨论】:

            猜你喜欢
            • 1970-01-01
            • 1970-01-01
            • 2013-04-06
            • 2020-08-15
            • 2019-08-24
            • 1970-01-01
            • 1970-01-01
            • 2014-03-28
            • 2016-05-16
            相关资源
            最近更新 更多