测试express中间件并捕获错误

测试express中间件并捕获错误,express,mocha.js,Express,Mocha.js,我试图断言在express中间件中异步抛出的错误: 要测试的中间件: const request = require('request'); const middleware = function (options) { if (!options) { throw new Error('Options are missing.'); // gets catched } request(options.uri, (err, response) => { if(e

我试图断言在express中间件中异步抛出的错误:

要测试的中间件:

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)

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

当您使用异步代码时,
try catch
&
throw
无法帮助您,因为它们只处理同步代码

解决方案

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;

现在,关于测试

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

  • 使用带有断言的
    try catch
    来处理来自中间件函数的
    错误
  • 使用普通断言处理来自express Middleware的错误


  • 注意我使用术语“快速中间件”仅指像
    函数(req,res,next){}
    这样的中间件。我使用术语“中间件”来指代称为
    中间件
    的函数。基本问题是,您从异步代码中抛出
    。简而言之:不要那样做,那很糟糕;-)

    所以,我想你有几个选择。哪一个最适合您取决于您的用例

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

    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调用,而是可以手动移交所需的结果。

    中间件中,从try-catch中返回一个承诺。因此,您的
    catch
    块将“拒绝(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()
                });
            });
        });
    })
    

    在返回中间件函数之前,请求的目的是什么?从OpenId Connect配置中检索稍后在中间件中调用的jwks URI。为什么不使用承诺?本质上,在中间件函数中,从try-catch返回一个承诺。因此,您的catch块将
    拒绝(err)
    @LostJon您介意提供一个工作示例吗?建议的代码对每个传入的HTTP请求执行HTTP请求,而原始代码仅在创建中间件时执行,这是另一回事。@robertklep说得很好。我会做些改变accordingly@robertklep我有个问题。如果HTTP请求只需要执行一次,那么为什么要将其与中间件关联呢?我可以在服务器启动时发出请求,并将其存储在DB或某个全局变量中。您必须向OP询问从中间件执行此操作的原理,但我同意您的看法,即在全局级别上执行此类操作更好。@BlazeSahlzen这确实是一个有效的问题。如果我将请求放在中间件之外,我不仅“拥有”了中间件的引导,而且还拥有了并非所有人都喜欢的应用程序本身。我编辑了您的答案,使其看起来像一个独立的答案。你最初的措辞让它看起来像是对评论的回复。如果它是对注释的回复,那么它也应该是一条注释,人们可以标记您的答案以删除。@Louis谢谢,注释格式在显示代码块时有点粗糙,所以我在回答中加入了它。我非常感谢您的编辑,我将在以后的回复中更加透彻。谢谢,我尝试过了,但这需要我首先调用
    中间件(选项)
    ,在解析中,我可以在express中注册内部返回函数(例如,对于express mw)。我不想以这种方式“拥有”mw注册过程。我想以这种方式注册mw:
    app.use(middleware(options))
    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()
                });
            });
        });
    })