【问题标题】:Testing express middleware and catch error测试 express 中间件并捕获错误
【发布时间】:2017-02-17 22:39:15
【问题描述】:

我正在尝试断言在快速中间件中异步抛出的错误:

要测试的中间件:

const request = require('request');
const middleware = function (options) {
  if (!options) {
    throw new Error('Options are missing.'); // gets catched
  }

  request(options.uri, (err, response) => {
    if(err) {
      throw err;
    }
  });

  return function (req, res, next) {}
}

module.exports = middleware;

mocha 测试:

describe('middleware', () => {
  describe('if async error is thrown', () => {
    it('should return an error', done => {
      try {
        middleware({
          uri: 'http://unkown'
        });
      } catch (err) {
        assert.equal('Error: getaddrinfo ENOTFOUND unkown unkown:80', err.toString());

        return done();
      }
    });
  });
})

问题是,err 没有在测试中被捕获:

Uncaught Error: getaddrinfo ENOTFOUND unkown unkown:80
      at errnoException (dns.js:27:10)
      at GetAddrInfoReqWrap.onlookup [as oncomplete] (dns.js:78:26)

我知道这是因为错误是异步抛出的,但我不知道如何在这里解决它。

【问题讨论】:

  • 返回中间件函数之前请求的目的是什么?
  • 从 OpenId Connect 配置中获取 jwks URI,稍后在中间件中调用。
  • 为什么不使用承诺?本质上在你的中间件函数中,从你的 try-catch 中返回一个 Promise。所以你的catch块会reject(err)
  • @LostJon 你介意提供一个工作样本吗?

标签: express mocha.js


【解决方案1】:

基本问题是您在异步代码中throw。简而言之:不要那样做,这很糟糕;-)

所以,恕我直言,您有几个选择。哪一个最适合您取决于您​​的用例。

场景一:异步设置函数

将您的同步转换为异步设置函数,即不要return 中间件函数,而是使用回调将其交给调用者。这样调用者的代码会变得更糟,但是你有一个干净的同步/异步代码拆分并且不要混合不适合混合的范例。

const middleware = function (options, callback) {
  if (!options) {
    throw new Error('Options are missing.');
  }

  request(options.uri, (error, response, body) => {
    if (error) {
      return callback(error);
    }

    callback(null, function (req, res, next) {
      // ...

      next();
    });
  });
};

场景 2:在每次调用中间件时执行请求

不要对request 进行一次调用,而是在每次中间件运行时进行。我不知道这是否有意义,但是这样你总是可以同步返回,只需要在中间件本身处理异步。

const middleware = function (options, callback) {
  if (!options) {
    throw new Error('Options are missing.');
  }

  return function (req, res, next) {
    request(options.uri, (error, response, body) => {
      if (error) {
        return next(error);
      }

      // ...

      next();
    });
  });
};

场景 3:外包请求

第三个,恕我直言,最好的选择是将请求外包,并将结果交给中间件的设置函数,而不是让设置函数执行请求。

这不仅解决了您的同步与异步问题,还使测试变得更容易,因为您不依赖 HTTP 调用,而是可以手动交出所需的结果。

【讨论】:

    【解决方案2】:

    当您使用异步代码时,try-catchthrow 无法帮助您,因为它们只处理同步代码。

    解决方法

    const request = require('request');
    
    const middleware = function(options) { // <--- outer scope
    
        var api_output = null; // Store the output of the HTTP request here
    
        if (!options) throw new Error('Options are missing.'); // Express shouldn't handle this error, hence we throw it (also this error is synchronous)
    
        return function(req, res, next) { // <--- inner scope
    
            if (!api_output) { // Check if the output of the HTTP request has already been saved (so as to avoid calling it repeatedly)
    
                request(options.uri, (error, response, body) => { // Perform the HTTP request
    
                    if (error) return next(error); // Pass the error to express error-handler
    
                    api_output = { // Save the output of the HTTP request in the outer scope
                        response: response,
                        body: body
                    };
    
                    req.api_output = api_output; // Set the output of the HTTP request in the req so that next middleware can access it
    
                    next(); // Call the next middleware
                });
    
            } else { // If the output of the HTTP request is already saved
    
                req.api_output = api_output; // Set the output of the HTTP request in the req so that next middleware can access it
    
                next(); // Call the next middleware
            }
        };
    }
    
    module.exports = middleware;
    

    我所做的只是返回一个 express-middleware,它只在尚未调用外部 API 时才调用它。如果没有错误,则将外部 API 的响应存储在 api_output 中,并将其传递给下一个可以使用的 express-middleware。


    要了解它的工作原理,必须了解 JavaScript 如何处理范围 (look up closure)。

    我没有在每次执行 express-middleware 时调用第 3 方 API,而是将该 API 的输出存储在 express-middleware 函数的外部范围中第一次调用它时 .这样做的好处是第 3 方 API 的输出对于外部和内部范围是私有的,并且无法从应用程序中的其他任何地方访问。


    如果有任何错误,则将其传递给next 回调(这会触发快速错误处理函数)。请参阅"Express error handling",了解其工作原理。特别是这个-

    如果您将任何内容传递给 next() 函数(字符串 'route' 除外),Express 会将当前请求视为错误,并将跳过任何剩余的非错误处理路由和中间件函数。

    您可以在创建路线时以这种方式使用上述功能:

    const router = require('express').Router();
    
    router
      .get('/my-route', middleware({
        uri: 'my-url'
      }), function(req, res, next) {
    
        // here you can access req.api_output.response & req.api_output.body
        // don't forget to call next once processing is complete
      });
    

    现在,关于测试

    在您的 mocha 测试框架中,您可以使用 request 模块调用您的 API,就像您在中间件函数中使用它来调用外部 API 一样。然后您可以轻松地断言输出:

    1. 使用带有断言的try-catch 来处理来自中间件函数的Error
    2. 使用正常的断言来处理来自 express-middlewares 的错误

    注意,我使用“express-middleware”一词仅指代function(req, res, next) {} 等中间件。我用“中间件”这个词来指代你的函数middleware

    【讨论】:

    • 建议的代码对每个传入的 HTTP 请求执行 HTTP 请求,而原始代码仅在创建中间件时执行,这是另一回事。
    • @robertklep 说得好。我会做出相应的改变
    • @robertklep 我有一个问题。如果 HTTP 请求只需要执行一次,那为什么还要将它与中间件联系起来呢?我可以简单地在服务器启动时发出请求并将其存储在数据库或某个全局变量中..
    • 您必须向 OP 询问从中间件执行此操作的理由,但我同意您的观点,这些事情最好在全球范围内完成。
    • @BlazeSahlzen 这确实是一个有效的问题。如果我将请求放在中间件之外,我不仅“拥有”了我的中间件的引导,而且还拥有了并非每个人都喜欢的应用程序本身。
    【解决方案3】:

    在您的 middleware 函数中,从您的 try-catch 返回一个承诺。所以你的catch 块会`reject(err)。

    我会这样做:

    const request = require('request');
    const middleware = function (options) {
        return new Promise((resolve,reject)=> {
            if (!options) {
                reject('Options are missing.'); 
            }
    
            request(options.uri, (err, response) => {
                if(err) {
                    reject(err)
                } else {
                    resolve(response)
                }
            });
        });
    }
    

    然后,对于您的测试用例,执行以下操作:

    describe('middleware', () => {
        describe('if async error is thrown', () => {
            it('should return an error', (done) => {
                middleware({uri: 'http://unkown'}).then((res)=>{
                    done(res)  //this will fail because you expect to never hit this block
                },(err)=>{
                    assert.equal('Error: getaddrinfo ENOTFOUND unkown unkown:80', err.toString());
                    done()
                });
            });
        });
    })
    

    【讨论】:

    • 我已经编辑了您的答案,使其看起来像一个独立的答案。您最初的措辞使潜在读者看起来像是对评论的回复。如果它是对评论的回复,那么它也应该是评论,人们可以将您的答案标记为删除。
    • @Louis 谢谢你,显示代码块时注释格式有点粗糙,所以我把它扔进了答案。无论哪种方式,我都感谢您的编辑,我会在以后的回复中更加彻底。
    • 谢谢,试过了,但它需要我先调用middleware(options),然后在解析中我可以在 express 中注册内部返回函数(例如 express mw)。我不想以这种方式“拥有”mw-registration 过程。
    • 我想这样注册mw:app.use(middleware(options));
    猜你喜欢
    • 1970-01-01
    • 2018-01-14
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2020-08-25
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多