Testning är en svår affär. Testning av fullstackade appar är ännu svårare. Du måste hantera frontend, backend, databas, nätverk och mer. Först självklart testar du dina komponenter, funktioner och moduler i isolering. Sedan skriver du integrationstester för att se till att de spelar bra tillsammans. Du kan till och med lägga till några end-to-end-tester för hela programmet för att simulera verkliga användarinteraktioner. Men då finns det kaosfaktorn: Vad händer när saker går fel? Vad händer när nätverket är långsamt eller opålitligt? Vad händer när backend är nere? En applikation som fungerar perfekt på den lyckliga vägen kan fortfarande lätt bryta när något oväntat händer. Chaos-driven testning är ett tillvägagångssätt som omfamnar denna osäkerhet genom att avsiktligt introducera misslyckanden i dina test. I den här artikeln kommer vi att undersöka hur man implementerar chaos-driven testning i en Next.js-applikation med hjälp av integrationstester som avsiktligt bryter saker och ting. Den app För att hålla artikeln fokuserad har jag skapat en minimal fullstack Next.js-app så att du inte behöver. Appen är en enkel receptapp där du kan bläddra i en lista över recept, visa receptdetaljer och liknande.Den använder Tailwind CSS för styling och TypeScript för typsäkerhet. När du gillar ett recept uppdaterar liktalen optimistiskt på frontenden medan backenden bearbetar begäran. Om backendsamtalet misslyckas återgår liktalen till sitt tidigare tillstånd. Om det lyckas returnerar det det nya liktalen, men frontenden uppdaterar inte det igen för att undvika att hoppa nummer om receptet gillades av en annan användare under tiden. Koden finns tillgänglig på . GitHub Kolla in repo, installera beroenden och du kan köra den lokalt med: git clone cd article-chaos-fetch npm install npm run dev Öppna i din webbläsare. http://localhost:3000 Du kommer att se något sånt här: Backend har tre API-vägar: GET /api/posts — lista alla recept GET /api/posts/[id] — få receptdetaljer POST /api/posts/[id]/likes — increment likes - returnerar det nya liket Observera att likningarna lagras i minnet och återställs vid omstart av servern. Detta är bara för demonstrationsändamål; i en riktig app skulle du använda en databas. En liknande knapp används i Användning av React och Det hanterar den optimistiska uppdateringen, felhanteringen och reversionslogiken. Förmodligen inte hur du eller jag skulle implementera den i en riktig app, men det kommer att göra för denna demo. src/components/LikeButton.tsx useState useEffect Enhetstest med Mock Service Worker (MSW) Om du vill testa din komponent finns det flera sätt att göra det, men ett av de smartaste sätten är att använda På så sätt kan du testa komponenten i isolering utan att förlita dig på den faktiska backenden. Mock Service Worker (MSW) är en I den Branch, vi har satt upp som testkörare och för rendering av komponenten och simulering av användarinteraktioner.Vi har också ställt in MSW för att avlyssna nätverksförfrågningar och returnera mock-svar. main Snabbast React testbibliotek Innehåller enhetstest för komponent. Det testar följande scenarier: src/components/LikeButton.test.tsx LikeButton Den liknande knappen inaktiverar under begäran och uppdaterar till backendvärdet på framgång. Den liknande knappen inaktiveras under begäran och rullar tillbaka på backend fel. Du kan köra testerna med: npm run test Om vi tar en titt på testkoden kan vi se hur vi använder MSW för att skämta bort backend-svaren.Till exempel, i det första testet överskrider vi skämtet för att returnera ett framgångsrikt svar med ett nytt like-tal på 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 }; }) ); ... I det andra testet överskrider vi mock för att returnera ett fel svar: 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 }; }) ); ... Vad MSW gör är att interceptera nätverksförfrågningar som görs av komponenten och returnerar de mock-svar vi definierade i testerna. På så sätt kan vi testa hur komponenten beter sig under olika backend-förhållanden utan att förlita sig på den faktiska backenden. LikeButton Integrationstest Så vad kan vi göra för att testa den fulla integrationen mellan frontend och backend? Tekniskt sett kan vi ändra MSW för att vidarebefordra förfrågningarna till den faktiska backend, men det skulle vara lite hacka och inte riktigt vad MSW är utformad för. eller för att köra end-to-end-tester och använda en fristående proxy som Eller något enklare som att simulera nätverksförhållanden - men det skulle vara lite överkill för den här enkla appen. Skådespelare Cypern toxiproxy Fördelar Proxy Detta är där kommer in. Det är ett lätt bibliotek som omsluter den inhemska API och låter dig införa kaos i dina nätverksförfrågningar.Du kan simulera latens, fel, hastighetsbegränsning, throttling och till och med slumpmässiga misslyckanden med bara några rader kod. Chaosfetch fetch Låt oss använda Om vi vill testa den fulla integrationen mellan frontend och backend måste vi köra testerna i en riktig webbläsarmiljö. miljö för detta, som simulerar en webbläsarliknande miljö i Node.js. chaos-fetch jsdom To use `chaos-fetch`, we first need to install it: ```bash npm install @fetchkit/chaos-fetch --save-dev Det första vi kan göra, för illustrationsändamål, är att byta MSW med Det är inte riktigt vad biblioteket är utformat för, men det fungerar. , vi ersätter MSW-inställningen med : 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 ... Som ni kan se skapar vi en klient som, i stället för att hämta, returnerar några mock data och ersätter den globala fungera med det. i den hook, vi återställer originalet funktion. Resten av testkoden förblir densamma. chaos-fetch fetch afterEach fetch Observera att vi också har lagt till en viss latens för att simulera en verklig nätverksförfrågan. komponenten inaktiverar knappen under begäran, och vi vill testa det beteendet. utan det skulle testet misslyckas eftersom begäran skulle slutföras för snabbt. (Och detta leder oss till den bräckliga, opålitliga världen av tidsbaserad testning, men det är ett ämne för en annan artikel.) LikeButton Koden för det andra testet är liknande; vi ändrar bara mock för att returnera ett fel: 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); ... Koden finns på den av repo. Du kan kolla in det med . tests-with-chaos-fetch git checkout tests-with-chaos-fetch Nu kan vi köra testerna med: npm run test Nu är MSW fortfarande en bättre passform för enhetstester, eftersom det är utformat för det ändamålet. För det också. chaos-fetch Chaos-driven integrationstest Om vi vill gå utöver enhetstester och testa den fulla integrationen mellan frontend och backend, kan vi använda för att införa kaos i våra nätverksförfrågningar. På så sätt kan vi testa hur appen beter sig under negativa förhållanden. chaos-fetch För det första måste vi för integrationstesterna göra en liten refaktor: vi kan inte direkt återge en asynkron serverkomponent i våra tester. En komponent som innehåller den och receptet detaljer. På så sätt kan vi testa i våra integrationstester. koden för är i . PostPage LikeButton LikeButton PostView src/components/PostView.tsx I vårt fall är det hela appen, vilket gör installationen rolig eftersom vi kommer att testa en komponent mot den app den är en del av, men du kan ställa in integrationstester på samma sätt om backenden är en separat tjänst. Samma tester som vi hade i är omskrivna i för att testa den fullständiga integrationen mellan frontend och backend.Koden är liknande, men istället för att skämta bort backend-svaren låter vi förfrågningarna gå igenom till den faktiska backenden. att införa fel i begäran. LikeButton.test.tsx PostView.integration.test.tsx chaos-fetch En viktig detalj är att vi måste till ett URL-objekt, så att kan lösa relativa webbadresser korrekt. I den här inställningen, JSDOM med den inbyggda hämtningen, skulle inte ens fungera för relativa webbadresser! Patches från JSDOM För att få det att fungera. globalThis.location chaos-fetch chaos-fetch location Först och främst sätter vi : globalThis.location globalThis.location = new URL("http://localhost:3000/posts/1"); Sedan skapar vi klienter i testerna som överskrider den inhemska hämtningen, och vi kan injicera latens, fel och mer. chaos-fetch För det första testet behöver vi inte ens ändra ; vi bara överskrider det så att det fungerar med relativa 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(); }); För det andra testet skapar vi en klient som simulerar ett backendfel för liknande begäran: 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(); }); Nu, tekniskt sett, i det andra testet, kallar vi inte backend alls eftersom Intercepterar förfrågan och returnerar ett fel, men ger ett enhetligt gränssnitt för att hantera framgångsrika och misslyckade förfrågningar på samma sätt. chaos-fetch Där detta tillvägagångssätt verkligen lyser är när du vill simulera mer komplexa nätverksförhållanden. middleware. Eller du kan prova vad som händer om din backend begränsar dig med och middleware. throttle rateLimit En annan sak som är svår att testa är om en fördröjd laddningsspinner visas medan begäran är i flygning. middleware för att simulera ett långsamt nätverk och testa att din laststatus visas korrekt. latency Om du inte bryr dig om determinism kan du till och med lägga till några slumpmässiga misslyckanden för att se hur din app beter sig under oförutsägbara förhållanden. Slutsats Kaostestning är ofta förknippad med storskaliga distribuerade system, men det är lika viktigt för mindre applikationer.I den här artikeln undersökte vi lätt kaosdriven testning för fullstack-appar med hjälp av integrationstester som avsiktligt bryter saker. för att införa kaos i våra nätverksförfrågningar och testa hur vår applikation beter sig under negativa förhållanden. chaos-fetch är inte en bättre (eller sämre) ersättning för MSW eller end-to-end testramar som Playwright eller Cypress. Genom att omfamna kaos och testa hur din applikation beter sig under misslyckade förhållanden kan du bygga mer motståndskraftiga och robusta applikationer. @fetchkit/chaos-fetch @fetchkit/chaos-proxy