【问题标题】:Mocking express-rate-limit for unit testing模拟单元测试的 express-rate-limit
【发布时间】:2020-07-29 18:26:01
【问题描述】:

我已经在我的应用程序中实现了“express-rate-limit”作为特定端点的中间件。在单元测试方面,我在尝试对这个中间件进行存根以适当地控制响应时遇到了麻烦。

例如,我有一个每 15 分钟允许 3 个请求的路由,但是,我有 10 个针对该路由的单元测试。前 3 个测试按预期通过,接下来的 7 个测试返回 '429 Too Many Requests' 响应。

'express-rate-limit' 库似乎被强烈建议用于速率限制,但是,我找不到任何关于如何在测试环境中使用它的信息。

下面显示了简化的实现尝试。

ratelimit.js

exports.createUser = rateLimit({
    windowMs: 15 * 60 * 1000, // 15 minute
    max: 3,
    message: 'Too many requests',
    statusCode: 429
})

route.js

const rateLimits = require('./ratelimit')

let incorrect = function (res, msg) {
    res.status(401)
    res.send({
        status: 'err',
        payload: {
            msg: msg
        }
    })
}

router.post('/create', rateLimits.createUser, (req, res) => {
    if (!req.body.email) return incorrect(res, 'Email not provided')
    userController
        .createUser(req.body.email)
        .then(user => {
            if (!user) incorrect(res, 'User not created')
            else correct(res)
        })
        .catch(() => incorrect(res, 'Internal Error'))
})

test.js

这应该将速率限制存根,只允许发出一个请求,这意味着第二个测试应该失败。但是第二个测试将通过,因为它从未被调用(根据 'real' 实现,速率限制仍然为 3)

const rateLimits = require('./ratelimit')
const userController = require('./usercontroller')
let server
let limiter

describe('Users', () => {
  before(() => {
    limiter = sinon.stub(rateLimits, 'createUser').callsFake(() => rateLimit({
      windowMs: 15 * 60 * 1000, // 15 minute
      max: 1,
      message: 'Too many requests',
      statusCode: 429
  }))
    server = require('../app')
  })
  

  it('should return an error for invalid email', done => {
    chai.request(server)
    .post('/users/create')
    .send({
      email: 'notanemail'
    })
    .end((err, res) => {
      res.should.have.status(401)
      res.body.status.should.equal('err')
      done()
    })
  })

  it('should return an error for missing parameter', done => {
    chai.request(server)
    .post('/users/create')
    .send({})
    .end((err, res) => {
      res.should.have.status(401)
      res.body.status.should.equal('err')
      done()
    })
  })

})

这个问题Stubbing Out Middleware 强调应该在执行存根后初始化应用程序本身,以确保正确加载。这似乎不起作用,在文件顶部创建导入的标准 .implementation 也不起作用。

另外,出于测试目的,我尝试在执行时将一些文本记录到控制台,以确保调用存根函数。该函数似乎永远不会被调用,因为日志永远不会打印到控制台。

  limiter = sinon.stub(rateLimits, 'createUser').callsFake(() => console.log("I was executed"))

我尝试的另一个失败的替代方法是直接调用 express-rate-limit 模块

const rateLimit = require('express-rate-limit')

limiter = sinon.stub(rateLimit.prototype, 'constructor').callsFake(() => rateLimit({
      windowMs: 15 * 60 * 1000, // 15 minute
      max: 1,
      message: 'Too many requests',
      statusCode: 429
  }))

【问题讨论】:

  • 好问题!感谢您提供所有详细信息。感谢您参考您尝试过的其他事情和其他 SO 答案。我希望所有问题都写得这么好!
  • @Hydralore 你能找到解决方案吗,我的问题是相关的,尽管我能够模拟 rateLimit 本身,我正在使用的存储(即 memcache-store)在单元中引起一些问题测试。如果您能分享您是如何实现这一目标的,将不胜感激。谢谢

标签: javascript node.js unit-testing


【解决方案1】:

这个问题已经 4 个月了,还没有收到回复,所以我会尽力解释它,因为我已经解决了它并得到了它的工作。

假设您正在使用这样的 express-rate-limit:

middleware.js

const rateLimit = require('express-rate-limit')

const limiter = rateLimit({
  windowMs: 60 * 60 * 1000 * 24, // 24 hour window
  max: 5, // start blocking after 5 requests
  message:
    'Too many requests sent from this IP, please try again after an hour'
})

module.exports = {
   limiter
}

app.js

const middleware = require('./middleware')

router.post('/my-rate-limited-api', middleware.limiter, async (req, res) => {
  try {
    const info = await doSomething('my args', req.body)
    res.json(info)
  } catch (err) {
    res.status(err.status || 400).send(`error: ${err.message}`)
  }
})

为了能够测试此路由中的内容,您需要存根限制器。

在您的测试文件中,确保在请求中包含一个 IP。在您的回复中,存根

req = {
      method: 'POST',
      ip: '12.12.123.123',
      url: '/my-rate-limited-api',
      body: [{ id: '123' }]
    }

res.headers = {}

res.setHeader = (x, y) => { res.headers[x] = y }

beforeEach(function () {
    const middleware = require('./middleware')
    this.sinon.stub(middleware, 'limiter').callsFake(function (req, res, next) {
      return next()
    })
    res = new Promise(function (resolve, reject) {
      doneResolve = this.resolve = resolve
      doneReject = this.reject = reject
    })
    res.headers = {}
    res.setHeader = (x, y) => { res.headers[x] = y }

    req = {
      method: 'POST',
      ip: '12.12.123.123',
      url: '/my-rate-limited-api',
      body: [{ id: '123' }]
    }

    next = this.sinon.spy(function createDeferredNext () {
      let resolve
      const promise = new Promise(function () {
        resolve = arguments[0]
      })
      const fn = (arg) => resolve(arg)
      fn.then = promise.then.bind(promise)
      return fn
    })
  })

从那里,您可以随心所欲地测试您的功能。


  it('can test without rate limits', function () {
    router(req, res, next)
    return res
      .then(ret => {
        expect(res.statusCode).to.eql(200)
      })
  })

【讨论】:

    【解决方案2】:

    最好的方法是检查您的应用程序是否在测试环境中运行,并为max 参数应用一些条件检查逻辑。假设您的请求是每 15 分钟窗口 5 个请求。您应该将 max 键设置为大于或等于测试环境中请求数的值。在这种情况下,我为测试环境使用了一个更大的数字,即 100。

    export const apiLimiter = rateLimit({
     windowMs: 15 * 60 * 1000, // 15 minutes
     max: process.env.NODE_ENV === "test" ? 100 : 3, // 3 requests per window
     onLimitReached: function (req, res /*, next*/) {
       throw new TooManyRequestsError();
     },
     handler: function (req, res /*, next*/) {
       throw new TooManyRequestsError();
     },
    
    });
    

    【讨论】:

    • 我对这个解决方案的唯一问题是您的原始代码必须更改以进行测试。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2022-01-26
    • 2018-10-25
    • 2013-02-22
    • 2011-04-11
    • 2020-05-04
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多