ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

使用 SuperTest 为 Express 路由与控制器编写单元测试:从模块拆分到请求断言

使用 SuperTest 为 Express 路由与控制器编写单元测试:从模块拆分到请求断言 使用 SuperTest 为 Express 路由与控制器编写单元测试从模块拆分到请求断言【免费下载链接】curriculumThe open curriculum for learning web development项目地址: https://gitcode.com/GitHub_Trending/cu/curriculum导读在 Express 应用中路由Route与控制器Controller承载了绝大部分业务逻辑而测试它们往往比测试纯函数更棘手——因为一个 HTTP 请求会完整地穿过中间件链、路由匹配与响应生成。本文以本仓库 NodeJS 课程体系为背景讲解如何使用supertest在不真正启动服务器的情况下对 Express 路由与控制器进行单元测试涵盖模块导出前提、app.listen规避、.expect()断言链、done回调以及基于 Promise 的请求编排。学完本文你将能够为任意 Express 路由编写可复现、可维护的测试用例并理解 SuperTest 与 SuperAgent 的关系。为什么需要单独测试 Express 路由与控制器单元测试的重要性无需赘述——如果你已经完成本仓库前端 JavaScript 课程中的测试章节应该已经接触过单元测试。本篇的目的不是重新讲授测试哲学或测试语法而是回答一个更具体的问题单元测试如何应用到 Express 应用与 API 上。Express 应用的请求处理链路是请求进入 → 应用级中间件 → 路由匹配 → 控制器本质上是终止请求-响应循环的中间件。整条链路的任意一环出错都会导致接口行为异常。因此对路由与控制器进行测试实际上是在验证这条链路的关键环节是否符合预期。本仓库的 controllers.md 明确指出控制器在 Express 世界中本身就是一种中间件由路由处理器使用负责从req中提取参数、调用模型/数据库、并通过res发送响应。测试路由与控制器就是验证“给定一个请求能否得到正确的响应”。前提被测试的代码必须位于导出的模块中对任何代码进行测试的最基本要求是它必须处于一个导出的模块中。这条规则对自定义中间件和路由/控制器同样适用。因此测试前的第一步是把路由和控制器从启动文件里分离出来放进各自的模块——如果它们还没被分离的话。在路由的场景下你已经知道如何用Express.Router完成这件事。本仓库的 routing.md 讲解了路由器的用法通过Router()创建路由实例在其上定义.get、.post等方法最后module.exports导出再在app.js中用app.use(/path, router)挂载。下面是一个最基础的示例。首先是应用的启动文件app.js//// app.js const express require(express); const app express(); app.use(express.urlencoded({ extended: false })); const indexRouter require(./index); app.use(/, indexRouter); app.listen(3000, (error) { if (error) { throw error; } console.log(running); });然后是独立的路由模块index.js//// index.js const express require(express); const index express.Router(); const array []; index.get(/, (req, res) { res.json({ name: frodo }); }); index.get(/test, (req, res) res.json({ array })); index.post(/test, (req, res) { array.push(req.body.item); res.send(success!); }); module.exports index;这两个文件定义了几条路由然后搭建并启动了 Express 应用。请注意这里的职责划分app.js目前不需要测试它只包含启动和运行 Express 应用的代码没有任何自有业务逻辑。它做的只是挂载中间件、挂载路由、调用app.listen。index.js包含需要测试的内容array状态、GET /、GET /test、POST /test这些路由处理逻辑都在这里。这种“启动文件与路由模块分离”的结构正是本仓库 project_mini_message_board.md 等项目中反复出现的模式——消息板项目同样把messages数组放在 index 路由器顶部并通过导出的路由器暴露接口。它让测试变得可行也让应用更易维护。可测试性设计要点如果路由处理器、控制器逻辑全部内联在app.js中且未被导出测试将无从下手。导出模块是单元测试的第一步也是代码结构是否健康的试金石。引入 SuperTest 与 Jest为了真正测试这些路由我们将使用一个名为SuperTest的库并且为了让测试语法更熟悉本课示例将其与Jest搭配使用。npm install jest supertest --save-dev安装的同时值得花几分钟浏览 SuperTest 的 README下文也会解释它的核心设计。需要强调SuperTest 与 Jest 是不同的东西Jest 提供测试运行器、断言与测试生命周期test、beforeEach等SuperTest 负责向 Express 应用发起真实的 HTTP 请求并断言响应而不需要真的把端口监听起来。它俩是协作关系Jest 驱动测试的执行SuperTest 提供请求与响应断言的 API。第一个测试文件完整示例下面是我们完整的测试文件const index require(../index); const request require(supertest); const express require(express); const app express(); app.use(express.urlencoded({ extended: false })); app.use(/, index); test(index route works, done { request(app) .get(/) .expect(Content-Type, /json/) .expect({ name: frodo }) .expect(200, done); }); test(testing route works, done { request(app) .post(/test) .type(form) .send({ item: hey }) .then(() { request(app) .get(/test) .expect({ array: [hey] }, done); }); });接下来我们逐步拆解这段代码。导入被测模块首先导入我们要测试的模块——也就是上面的index.jsconst index require(../index);这是整个测试的入口没有这个导入测试就无从谈起。被测对象必须是可导入、可挂载的模块这再次印证了第一节的“导出前提”。为什么在测试里新建一个 Express 应用接着引入supertest和express并新建一个 Express 应用把之前导入的 index 路由器挂载上去const request require(supertest); const express require(express); const app express(); app.use(express.urlencoded({ extended: false })); app.use(/, index);为什么必须在这里重新搭一个 app而不是直接测原来的app.js避免调用app.listen、不启动真实服务器这是最主要的原因。SuperTest 可以把 Express 应用对象直接作为请求目标在内存中模拟 HTTP 请求因此完全不需要占用端口。这也意味着测试更快、更干净不会与开发中的服务器冲突。按需组装应用在更大的应用中我们可以跳过一些可选的配置步骤只包含测试所必需的中间件。例如这里只需要express.urlencoded来解析表单体POST /test依赖req.body.item就不需要挂载静态资源、会话等与测试无关的中间件。隔离性测试环境与生产启动逻辑解耦避免app.js中的任何启动副作用污染测试。express.urlencoded({ extended: false })的作用是把表单提交的数据解析进req.body这正是本仓库 forms_and_data_handling.md 与 project_mini_message_board.md 中处理表单的标准姿势——在消息板项目中同样的中间件被用于把input namemessageText的值解析为req.body.messageText。测试中必须复现这一中间件否则POST /test拿不到req.body断言必然失败。在更大的测试套件中这段“组装 app”的代码很可能被抽象成独立文件例如testApp.js被每个测试文件导入复用——这与路由、控制器被拆分成模块的思路一脉相承。第一个测试GET 请求与.expect()断言链test(index route works, done { request(app) .get(/) .expect(Content-Type, /json/) .expect({ name: frodo }) .expect(200, done); });得益于 SuperTest测试本身相当直白我们把request函数调用在我们的 Express 应用上传入路由然后使用.expect()断言响应与期望的类型和内容一致。逐行解读这条断言链request(app)发起针对该 app 的请求。.get(/)指定 HTTP 方法与路径等价于向GET /发请求。.expect(Content-Type, /json/)断言响应头Content-Type匹配正则/json/。这里用正则而非精确字符串是因为 Express 实际返回的可能是application/json; charsetutf-8正则匹配更宽容、更稳健。.expect({ name: frodo })断言响应体是一个对象{ name: frodo }——与index.js中GET /返回的res.json({ name: frodo })完全对应。.expect(200, done)断言状态码为 200并把done回调交给 SuperTest 代为调用。done参数异步测试的完成信号注意传入测试回调的done参数。大多数测试库用它来标记异步操作的测试完成时机当done()被调用时Jest 才会认为该测试结束若不调用测试将一直挂起直至超时。SuperTest 提供的一个便利之处是你可以把done直接传入最后一个.expect()SuperTest 会在断言全部通过后替你调用它.expect(200, done);等价于手动写法.expect(200) .end((err) { if (err) throw err; done(); });关于done与.end()的差异需要特别注意错误处理方式使用.end(callback)时断言失败的错误会作为回调参数传入需要你手动处理而把done直接传给.expect()SuperTest 会在断言失败时直接把错误交给测试框架。理解这两种写法的区别是知识检查中的重点之一。第二个测试POST Promise 编排第二个测试与第一个很相似但测试的是post方法test(testing route works, done { request(app) .post(/test) .type(form) .send({ item: hey }) .then(() { request(app) .get(/test) .expect({ array: [hey] }, done); }); });.post(/test)向POST /test发送请求。.type(form)把请求体编码类型设置为表单application/x-www-form-urlencoded确保服务端的express.urlencoded中间件能正确解析。.send({ item: hey })发送请求体数据对应index.js中array.push(req.body.item)要读取的req.body.item。最后一段很重要SuperTest 的请求返回一个 Promise底层由 SuperAgent 提供。在这里我们等待 POST 请求完成当该 Promise 解析后再发起 GET 请求检查item是否真的被 push 进了array.then(() { request(app) .get(/test) .expect({ array: [hey] }, done); });这验证的是状态变化POST 写入数据 → GET 读取数据 → 断言数据确实被写入。这类“先写后读”的测试是路由集成测试的典型形态。SuperTest 与 SuperAgent一脉相承的 APISuperTest 实际上从另一个相关项目SuperAgent中获取了核心能力。二者关系可以概括为SuperAgent是一个 Node.js 端的 HTTP 客户端库负责实际的请求发送与响应解析。SuperTest构建在 SuperAgent 之上把“向真实 HTTP 服务器发请求”的能力“重定向”为“直接向 Express 应用对象发请求”。凡是在 SuperAgent 中可调用的方法在 SuperTest 中同样可以调用。这意味着SuperTest 的能力边界不止于本课示例中的.get()、.post()、.type()、.send()、.expect()。举一些常用的扩展场景Multipart 请求SuperAgent 提供了处理 multipart 请求的方法如attach()附加文件、field()附加表单字段SuperTest 同样可用——这也是知识检查中要求掌握的点。请求头与认证可通过.set()设置自定义请求头、携带 Authorization 凭证。重定向与超时可通过.redirects()、.timeout()等方法控制请求行为。建议的查阅路径先通读 SuperTest 的文档README掌握其核心 API再浏览 SuperAgent 的文档以了解所有可用的请求方法。本仓库的 testing_database_operations.md 还展示了 SuperTest 在集成测试场景下的进一步用法配合测试数据库与 Prisma。数据层测试的注意事项如果我们的代码使用了真实数据库那么测试时就应该采用测试数据库或模拟数据库相关内容会在后续独立课程中展开。现阶段只需牢记一条铁律绝不要在生产数据库上运行测试代码在 Express 项目中测试数据库的配套方案如以test_前缀命名独立数据库、通过TEST_DATABASE_URL环境变量切换连接串、用beforeEach重置表数据、为 Jest 添加--runInBand保证串行执行等详见 testing_database_operations.md。本课的路由测试全部基于内存中的array天然避开了数据库污染问题——这也是把可变状态收进模块、用 SuperTest 做内存请求的好处之一。课程回顾与自检清单完成本课学习后你应该能回答以下问题也可作为自测SuperTest 存在的动机是什么它允许开发者在不启动真实 HTTP 服务器的情况下用接近真实请求的方式测试 Express 应用——直接向应用对象注入请求并断言响应。done的用途是什么SuperTest 提供了什么便利done用于向测试框架报告异步测试完成SuperTest 允许把它直接传给最后一个.expect()由库自动调用。.end()与.expect()配合使用时的错误处理差异.end(callback)会把断言错误作为回调参数传入需要手动处理直接传done给.expect()则由库代为抛给测试框架。SuperAgent 提供了哪些处理 multipart 请求的方法例如attach()与field()且这些方法在 SuperTest 中同样可用。此外本课的核心结论可以浓缩为三条工程实践分离即测试的前提把路由/控制器放入导出的模块如index.js启动逻辑留在app.js组装最小化测试应用在测试文件中新建 Express 应用仅挂载测试所需中间件与路由器绕开app.listen断言与编排用.expect()链断言状态码、响应头正则匹配与响应体用 Promise 链编排“写后读”等多步请求。将这些实践应用到本仓库 project_mini_message_board.md、project_inventory_application.md 等项目上你就能为消息板、库存管理等真实 Express 应用搭建起第一层自动化的路由与控制器测试防线。【免费下载链接】curriculumThe open curriculum for learning web development项目地址: https://gitcode.com/GitHub_Trending/cu/curriculum创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表