测试是一个棘手的业务。测试全套应用程序甚至更棘手。您必须处理前端、后端、数据库、网络等等。首先,当然,您单独测试您的组件、功能和模块。 但是接下来还有混乱因素:当事情发生错误时会发生什么?当网络慢或不可靠时会发生什么?当后端失效时会发生什么?在幸福道路上完美运作的应用程序在意外发生时仍然可以轻松打破。 混沌驱动测试是一种方法,通过故意引入测试中的失败来拥抱这种不确定性,在本文中,我们将探索如何在Next.js应用程序中实施混沌驱动的测试,使用故意破坏事情的集成测试。 这个app 好吧,首先我们需要一个应用程序来测试。为了保持文章的重点,我创建了一个最小的完整的Next.js应用程序,所以你不必。 该应用程序是一个简单的食谱应用程序,您可以浏览食谱列表,查看食谱细节,并喜欢它们。 当你喜欢一个食谱时,类似计数在前端上乐观地更新,而后端处理请求。如果后端调用失败,类似计数将返回以前的状态。 代码可在 . 吉普赛 查看 Repo,安装依赖,您可以通过: git clone cd article-chaos-fetch npm install npm run dev 开放 在你的浏览器中 http://localhost:3000 你会看到这样的东西: 后端有三个 API 路径: GET /api/posts — 列出所有食谱 GET /api/posts/[id] — 获取食谱细节 POST /api/posts/[id]/like — 增加喜欢 - 返回新的喜欢数 请注意,喜欢被存储在内存中,并在重新启动服务器时重新设置,这只是用于演示目的;在真正的应用程序中,你会使用数据库。 类似的按钮应用于 使用 React 的 和 它处理了乐观的更新,错误处理和逆转逻辑. 可能不是你或我如何在一个真正的应用程序中实现它,但它会为这个演示。 src/components/LikeButton.tsx useState useEffect 单元测试与模糊服务工人(MSW) 如果你想单独测试你的组件,有许多方法可以做到这一点,但最聪明的方法之一是使用 以此方式,您可以单独测试组件,而不依赖实际的后端。 摩克服务工人(MSW) 在该 Branch,我们建立了 作为测试跑者和 我们还设置了MSW来拦截网络请求并返回模拟响应。 main 速度 React 测试图书馆 它包含了单元测试的 组件. 它测试了以下场景: src/components/LikeButton.test.tsx LikeButton 类似按钮在请求期间禁用,并在成功时更新到后端值。 类似按钮在请求期间禁用,并在后端错误时返回。 您可以运行测试与: npm run test 如果我们看看测试代码,我们可以看到我们如何使用MSW来嘲笑后端响应,例如,在第一个测试中,我们忽略了模糊,以返回一个成功的响应,新的像数为42: // test("like button disables during request and updates to backend value" ... server.use( http.post("/api/posts/:id/like", async () => { await new Promise((resolve) => setTimeout(resolve, 100)); return { likes: 42 }; }) ); ... 在第二次测试中,我们将模糊排除,以返回错误响应: test("like button disables during request and rolls back on backend error" ... server.use( http.post("/api/posts/:id/like", async () => { await new Promise((resolve) => setTimeout(resolve, 100)); return { status: 500 }; }) ); ... MSW所做的就是拦截网络请求。 通过这种方式,我们可以测试组件如何在不同的后端条件下行为,而不依赖实际的后端,但是没有真正的后端参与,所以我们无法测试前端和后端之间的完整集成! LikeButton 整合测试 那么,我们可以做些什么来测试前端和后端之间的完整集成呢?从技术上讲,我们可以更改MSW以将请求转发到实际的后端,但这会有点混乱,而不是真正的MSW是为了什么而设计的。 或 运行端到端测试并使用独立的代理,如 或者更简单的像 模拟网络条件 - 但这对于这个简单的应用程序来说会有点过分。 演员 塞浦路斯 毒品 混沌的代理 这是哪里 它是一个轻量级的图书馆,包裹着原住民 API 并允许您在网络请求中引入混乱,您可以模拟延迟、错误、速度限制、推移甚至随机故障,仅使用几行代码。 混沌的 fetch 让我们使用 如果我们想测试前端和后端之间的完整集成,我们需要在真实的浏览器环境中运行测试。 为此,它模拟了 Node.js 中的浏览器环境。 chaos-fetch jsdom To use `chaos-fetch`, we first need to install it: ```bash npm install @fetchkit/chaos-fetch --save-dev 我们可以做的第一件事,为说明目的,是交换MSW与 在我们的单元测试中,这不是真正的图书馆是为何而设计的,但它工作。 ,我们将MSW设置替换为 : chaos-fetch LikeButton.test,tsx chaos-fetch // src/components/LikeButton.test.tsx import { createClient, replaceGlobalFetch, restoreGlobalFetch, } from "@fetchkit/chaos-fetch"; ... describe("LikeButton", () => { afterEach(() => { restoreGlobalFetch(); }); test("like button disables during request and updates to backend value", async () => { // Mock fetch to return success const client = createClient( { global: [ { latency: { ms: 300 } }, ], routes: { "POST /api/posts/:id/like": [ { latency: { ms: 300 } }, { mock: { body: '{ "likes": 43 }' } }, ], }, }, window.fetch ); // Replace global fetch with mock client replaceGlobalFetch(client); // From here on, the test code remains the same ... 正如你所看到的,我们创建了一个 客户端,而不是接收,返回一些模糊数据,并取代全球数据。 与之相处,在 胡克,我们恢复原始 功能. 测试代码的其余部分保持相同。 chaos-fetch fetch afterEach fetch 注意,我们还添加了一些延迟来模拟真实的网络请求。 如果没有它,测试会失败,因为请求会很快完成(这导致我们进入时间测试的脆弱,不可靠的世界,但这是另一个文章的主题)。 LikeButton 第二次测试的代码类似,我们只需更改模糊来返回错误: test("like button disables during request and rolls back on backend error", async () => { // Mock fetch to return success const client = createClient( { global: [], routes: { "POST /api/posts/:id/like": [ { latency: { ms: 300 } }, { mock: { status: 500, body: '{ "error": "Internal Server Error" }' } }, ], }, }, window.fetch ); // Replace global fetch with mock client replaceGlobalFetch(client); ... 代码是在 雷锋的分支,你可以检查一下。 . tests-with-chaos-fetch git checkout tests-with-chaos-fetch 现在,我们可以运行测试与: npm run test 现在,MSW仍然更适合单元测试,因为它是专为此目的而设计的。 为此也。 chaos-fetch 混沌驱动的集成测试 如果我们想超越单元测试并测试前端和后端之间的完整集成,我们可以使用 通过这种方式,我们可以测试应用程序在不利条件下如何行为。 chaos-fetch 首先,对于集成测试,我们必须做一个小重构器:我们不能直接在我们的测试中渲染非同步服务器组件。 包含的组件 食谱的细节,这样我们就可以测试。 在我们的集成测试中,代码为 已在 . PostPage LikeButton LikeButton PostView src/components/PostView.tsx 接下来,我们必须确保后端在我们运行测试之前运行,在我们的情况下,它是整个应用程序,这使得设置变得有趣,因为我们将测试一个组件与它的一部分应用程序,但如果后端是一个单独的服务,您可以以同样的方式设置集成测试。 同樣的測試我們在 被重写在 测试前端和后端之间的完整集成 代码类似,但不是嘲笑后端响应,我们让请求通过到实际的后端。 在请求中引入错误。 LikeButton.test.tsx PostView.integration.test.tsx chaos-fetch 一个重要的细节是,我们必须设置 一个 URL 对象,所以 可以正确地解决相对 URL。在这个设置中,JSDOM 与原生 fetch,甚至不会对相对 URL 工作! JSDOM 的 Patches 让它工作。 globalThis.location chaos-fetch chaos-fetch location 首先,我们设 : globalThis.location globalThis.location = new URL("http://localhost:3000/posts/1"); 然后,我们创造了 客户在测试中超越了原生接收,我们可以注射延迟、错误等等。 chaos-fetch 对于第一个测试,我们甚至不需要修改 ;我们只排列它,以便它与相对 URL 一起工作: fetch test("integration: like button disables during request and re-enables after fetch (real backend)", async () => { replaceGlobalFetch(createClient({})); render(<PostView postId={1} />); // Wait for post to load (like count should be present) const likeCountText = await screen.findByText(/\d+\s*likes/); const initialCount = Number(likeCountText.textContent.match(/(\d+)/)?.[1] ?? 0); const button = await screen.findByRole("button", { name: /like/i }); const user = userEvent.setup(); await user.click(button); // Button should be disabled during request expect(button).toBeDisabled(); // Wait for fetch to complete and UI to update await waitFor(() => expect(button).not.toBeDisabled()); // Check updated like count await waitFor(() => { const updatedLikeCountText = screen.getByText(/\d+\s*likes/); const updatedCount = Number(updatedLikeCountText.textContent.match(/(\d+)/)?.[1] ?? 0); expect(updatedCount).toBe(initialCount + 1); }); restoreGlobalFetch(); }); 对于第二次测试,我们创建了一个模拟类似请求的后端错误的客户端: test("integration: like button disables during request and rolls back on backend error (fail middleware)", async () => { // Configure chaos-fetch to fail the like endpoint replaceGlobalFetch(createClient({ routes: { "POST /api/posts/:id/like": [ { latency: { ms: 300 } }, { fail: { status: 500, body: '{ "error": "fail middleware" }' } }, ], }, })); render(<PostView postId={1} />); // Wait for post to load (like count should be present) const likeCountText = await screen.findByText(/\d+\s*likes/); const initialCount = Number(likeCountText.textContent.match(/(\d+)/)?.[1] ?? 0); const button = await screen.findByRole("button", { name: /like/i }); const user = userEvent.setup(); await user.click(button); // Button should be disabled during request expect(button).toBeDisabled(); // Wait for fetch to complete and UI to update await waitFor(() => expect(button).not.toBeDisabled()); // Check that like count rolls back to original value await waitFor(() => { const rolledBackLikeCountText = screen.getByText(/\d+\s*likes/); const rolledBackCount = Number(rolledBackLikeCountText.textContent.match(/(\d+)/)?.[1] ?? 0); expect(rolledBackCount).toBe(initialCount); }); restoreGlobalFetch(); }); 现在,从技术上讲,在第二次测试中,我们根本不叫后端,因为 拦截请求并返回错误,但它提供了一个统一的界面来处理成功和失败的请求。 chaos-fetch 这种方法真正亮的是当你想模拟更复杂的网络条件时。 或者你可以尝试发生什么,如果你的后端限制你与 中间人。 throttle rateLimit 另一个很难测试的事情是,请求在飞行时是否显示延迟的加载旋转器。 模拟一个缓慢的网络,并测试您的加载状态是否正确显示。 latency 如果你不关心定义,你甚至可以添加一些随机故障,以查看你的应用程序在不可预测的情况下如何行为. 你也可以写和注册自己的自定义中间软件来模拟特定的场景。 结论 混乱测试通常与大规模的分布式系统有关,但对于小型应用程序同样重要. 在本文中,我们探讨了轻量级混乱驱动的全套应用程序测试,使用集成测试故意破坏事物。 在我们的网络请求中引入混乱,并测试我们的应用程序在不利条件下如何行为。 chaos-fetch 它不是 MSW 或 Playwright 或 Cypress 等端到端测试框架的更好的(或更糟糕的)替代品。 通过拥抱混乱并测试您的应用程序在失败条件下如何行为,您可以构建更具弹性和更强大的应用程序。 @fetchkit/chaos-fetch @fetchkit/chaos-proxy