如何测试可由多个代码路径触发的错误路径的 API 端点?
How to test an API endpoint for an error path that can be triggered by more than one code paths?
比如说,函数中有多个路径会导致服务器错误 (500)。所以在单元测试中:
- 仅从任何单一路径触发错误就足够了吗?
- 在断言时,仅针对 500 错误代码进行断言就足够了,还是还需要针对预期错误进行断言?
我的意思(在代码中)?
it('should trigger server error', async function() {
// arrange
const res = {
status: this.sandbox.stub().returnsThis(),
json: this.sandbox.stub(),
};
const req = {
body: { name: 'foo', email: 'foo@bar.com', password: 'password' },
};
// switch on one possible trigger path that would lead to 500 error
const expectedError = new Error('Server Error');
this.sandbox.stub(User, 'createWithPassword').rejects(expectedError);
// act
await userApi.create(req, res);
// assert
// is this assertion enough ?
expect(res.status).to.have.been.calledWith(500);
// also should this go along with the above?
expect(res.json).to.have.been.calledWith(expectedError);
});
不,您应该测试每条路径。您甚至应该 运行 在代码覆盖工具下进行单元测试(例如 istanbul) so you make sure to cover all paths. This is all about cyclomatic complexity;您拥有的路径越多,您需要覆盖所有内容的测试就越多。最好保持低水平。
仅针对错误类型断言是可以的,但您最好针对每个特定错误进行断言,以确保随着时间的推移您不会引入导致一个条件抛出另一个错误的错误。在这种情况下,您将获得假阳性测试通过。
分离错误处理和您的用户 controller/business 逻辑。您的用户控制器不必担心处理错误。
编写单独的错误处理中间件来处理任何错误:
module.handleErrors = (error, req, res, next) => {
// Handle error
// ...
res.status(500).json()
}
但是现在我们需要一种方法来捕获控制器中发生的任何错误。所以我们需要另一个中间件来捕获错误并将错误转发给上面的错误处理中间件:
module.catchErrors = controllerAction => (req, res, next) => controllerAction(req, res).catch(next)
现在我们可以从全局错误处理程序开始连接中间件:
const express = require('express)
const { handleErrors } = require('./error-middleware)
const { userRoutes } = require('./user-routes)
const app = express()
app.use('/users', userRoutes)
app.use(handleErrors)
// ..
接下来连接用户路由和控制器以捕获错误:
const express = require('express')
const userController = require('./user-controller')
const { catchErrors } = require('./error-middleware')
const router = express.Router()
// Passing the createWithPassword function to the catch error middleware.
router.post('/', catchErrors(userController.createWithPassword))
module.exports = router
现在一切都已解耦,您可以自由地单独测试每个部分以进行单元测试。集成测试是您测试许多场景的地方。所以:
- 使用我描述的上述设置,我们不需要测试每个场景,因为最终任何错误都会被错误处理中间件捕获。我们只关心出现错误时是否处理得当。
- 这取决于您的要求。如果您所做的只是抛出一个没有明确 message/reason 的异常,那么检查
500
和类型就足够了。但是,如果您期望在 specific 操作期间发生 specific 错误,那么将测试错误消息、异常、HTTP 状态,等等。
比如说,函数中有多个路径会导致服务器错误 (500)。所以在单元测试中:
- 仅从任何单一路径触发错误就足够了吗?
- 在断言时,仅针对 500 错误代码进行断言就足够了,还是还需要针对预期错误进行断言?
我的意思(在代码中)?
it('should trigger server error', async function() {
// arrange
const res = {
status: this.sandbox.stub().returnsThis(),
json: this.sandbox.stub(),
};
const req = {
body: { name: 'foo', email: 'foo@bar.com', password: 'password' },
};
// switch on one possible trigger path that would lead to 500 error
const expectedError = new Error('Server Error');
this.sandbox.stub(User, 'createWithPassword').rejects(expectedError);
// act
await userApi.create(req, res);
// assert
// is this assertion enough ?
expect(res.status).to.have.been.calledWith(500);
// also should this go along with the above?
expect(res.json).to.have.been.calledWith(expectedError);
});
不,您应该测试每条路径。您甚至应该 运行 在代码覆盖工具下进行单元测试(例如 istanbul) so you make sure to cover all paths. This is all about cyclomatic complexity;您拥有的路径越多,您需要覆盖所有内容的测试就越多。最好保持低水平。
仅针对错误类型断言是可以的,但您最好针对每个特定错误进行断言,以确保随着时间的推移您不会引入导致一个条件抛出另一个错误的错误。在这种情况下,您将获得假阳性测试通过。
分离错误处理和您的用户 controller/business 逻辑。您的用户控制器不必担心处理错误。
编写单独的错误处理中间件来处理任何错误:
module.handleErrors = (error, req, res, next) => {
// Handle error
// ...
res.status(500).json()
}
但是现在我们需要一种方法来捕获控制器中发生的任何错误。所以我们需要另一个中间件来捕获错误并将错误转发给上面的错误处理中间件:
module.catchErrors = controllerAction => (req, res, next) => controllerAction(req, res).catch(next)
现在我们可以从全局错误处理程序开始连接中间件:
const express = require('express)
const { handleErrors } = require('./error-middleware)
const { userRoutes } = require('./user-routes)
const app = express()
app.use('/users', userRoutes)
app.use(handleErrors)
// ..
接下来连接用户路由和控制器以捕获错误:
const express = require('express')
const userController = require('./user-controller')
const { catchErrors } = require('./error-middleware')
const router = express.Router()
// Passing the createWithPassword function to the catch error middleware.
router.post('/', catchErrors(userController.createWithPassword))
module.exports = router
现在一切都已解耦,您可以自由地单独测试每个部分以进行单元测试。集成测试是您测试许多场景的地方。所以:
- 使用我描述的上述设置,我们不需要测试每个场景,因为最终任何错误都会被错误处理中间件捕获。我们只关心出现错误时是否处理得当。
- 这取决于您的要求。如果您所做的只是抛出一个没有明确 message/reason 的异常,那么检查
500
和类型就足够了。但是,如果您期望在 specific 操作期间发生 specific 错误,那么将测试错误消息、异常、HTTP 状态,等等。