Testning er en besværlig forretning. Testning af fuldstack-apps er endnu sværere. Du skal beskæftige dig med frontend, backend, database, netværk og meget mere. Først, selvfølgelig, tester du dine komponenter, funktioner og moduler isoleret. Derefter skriver du integrationstests for at sikre, at de spiller godt sammen. Du kan endda tilføje et par end-to-end-tests til hele applikationen for at simulere reelle brugerinteraktioner. Men så er der kaosfaktoren: Hvad sker der, når tingene går galt? Hvad sker der, når netværket er langsomt eller upålideligt? Hvad sker der, når backend er nede? En applikation, der fungerer perfekt på den lykkelige vej, kan stadig nemt bryde, når noget uventet sker. Chaos-drevet test er en tilgang, der omfavner denne usikkerhed ved bevidst at indføre fejl i dine tests.I denne artikel vil vi undersøge, hvordan man implementerer chaos-drevet test i en Next.js-applikation ved hjælp af integrationstests, der bevidst bryder tingene. Den app For at holde artiklen fokuseret, har jeg skabt en minimal full-stack Next.js app, så du ikke behøver. Appen er en simpel opskrift app, hvor du kan gennemse en liste over opskrifter, se opskrift detaljer, og som dem. Når du kan lide en opskrift, opdaterer like count optimistisk på frontend, mens backend behandler anmodningen. Hvis backend-opkaldet mislykkes, vender like count tilbage til sin forrige tilstand. Hvis det lykkes, returnerer det den nye like count, men frontend opdaterer det ikke igen for at undgå at hoppe tal, hvis opskriften blev nydt af en anden bruger i mellemtiden. Dette er et almindeligt mønster i webapps for at give en hurtig brugeroplevelse, når konsistens ikke er kritisk. Koden er tilgængelig på . af Github Tjek repoen, installer afhængigheder, og du kan køre den lokalt med: git clone cd article-chaos-fetch npm install npm run dev Åbent i din browser. http://localhost:3000 Du vil se noget som dette: Backend har tre API ruter: GET /api/posts — liste alle opskrifter GET /api/posts/[id] — få oplysninger om opskriften POST /api/posts/[id]/like — increment likes - returnerer den nye like count Bemærk, at likes gemmes i hukommelsen og nulstilles ved genstart af serveren. Dette er kun til demonstrationsformål; i en reel app ville du bruge en database. Sådan en knap kan anvendes i Brug af React og Det håndterer den optimistiske opdatering, fejlhåndtering og reversionslogik. Sandsynligvis ikke hvordan enten du eller jeg ville implementere det i en rigtig app, men det vil gøre for denne demo. src/components/LikeButton.tsx useState useEffect Enhedstest med Mock Service Worker (MSW) Hvis du vil teste din komponent, er der flere måder at gøre det på, men en af de smarteste måder er at bruge På denne måde kan du teste komponenten i isolation uden at stole på den faktiske backend. Mock Service Worker (MSW) er en I den Branch, vi har oprettet Som testløber og Vi har også oprettet MSW til at aflyse netværksforespørgsler og returnere mock-svar. main Hurtigere React test bibliotek Indeholder enhedstest for komponent. Det tester følgende scenarier: src/components/LikeButton.test.tsx LikeButton Like-knappen deaktiverer under forespørgslen og opdaterer til backendværdien på succes. Like-knappen deaktiveres under anmodningen og ruller tilbage på backend-fejl. Du kan køre testen med: npm run test Hvis vi har et kig på testkoden, kan vi se, hvordan vi bruger MSW til at spotte backend-svarene. For eksempel, i den første test, overrider vi spottet for at returnere et vellykket svar med en ny like-tælling 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 den anden test overskrider vi mock for at returnere en fejlrespons: 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 }; }) ); ... Hvad MSW gør er at aflyse netværksforespørgsler foretaget af På denne måde kan vi teste, hvordan komponenten opfører sig under forskellige backend-forhold uden at stole på den faktiske backend. LikeButton Integrationstest Så hvad kan vi gøre for at teste den fulde integration mellem frontend og backend? Teknisk set kunne vi ændre MSW til at videresende anmodningerne til den faktiske backend, men det ville være lidt hacky og ikke rigtig hvad MSW er designet til. eller at køre end-to-end-tests og bruge en standalone proxy som Eller noget så simpelt som at simulere netværksforhold - men det ville være lidt overkill for denne enkle app. af Playwright Cypern Toksikologisk Køgeproxy Det er her, hvor Det er et let bibliotek, der omslutter den indfødte API og giver dig mulighed for at indføre kaos i dine netværksforespørgsler. Du kan simulere latens, fejl, hastighedsbegrænsning, throttling og endda tilfældige fejl med kun et par linjer kode. Chaosfænomenet fetch Lad os bruge Hvis vi vil teste den fulde integration mellem frontend og backend, skal vi køre testene i et reelt browsermiljø. miljø for dette, som simulerer et browserlignende 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ørste, vi kan gøre, for illustration, er at udveksle MSW med Det er ikke rigtigt, hvad biblioteket er designet til, men det virker. , vi erstatter MSW-opstillingen 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 du kan se, skaber vi en klient, der i stedet for at hente, returnerer nogle mock data og erstatter den globale Arbejdet med det. i Hook, vi genopretter den oprindelige Resten af testkoden er den samme. chaos-fetch fetch afterEach fetch Bemærk, at vi også har tilføjet en vis latens for at simulere en reel netværksforespørgsel. komponenten deaktiverer knappen under anmodningen, og vi ønsker at teste den adfærd. Uden det ville testen mislykkes, fordi anmodningen ville være færdig for hurtigt. (Og dette fører os til den skrøbelige, upålidelige verden af tidsbaseret test, men det er et emne for en anden artikel.) LikeButton Koden for den anden test er tilsvarende; vi ændrer bare mock for at returnere en fejl: 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 er på den Rygsøjlen. - Du kan tjekke den med . tests-with-chaos-fetch git checkout tests-with-chaos-fetch Nu kan vi køre testene med: npm run test Nu er MSW stadig en bedre egnet til enhedstest, fordi det er designet til det formål. Også for det. chaos-fetch Chaosdrevet integrationstest Hvis vi vil gå ud over enhedstests og teste den fulde integration mellem frontend og backend, kan vi bruge at indføre kaos i vores netværksanmodninger. På denne måde kan vi teste, hvordan applikationen opfører sig under ugunstige forhold. chaos-fetch For det første skal vi for integrationstestene lave en lille refaktor: Vi kan ikke direkte gengive en asynkron serverkomponent i vores tests. En komponent, der indeholder og opskriften detaljer. På denne måde kan vi teste i vores integrationstest. koden for er i . PostPage LikeButton LikeButton PostView src/components/PostView.tsx I vores tilfælde er det hele appen, hvilket gør opsætningen sjovt, fordi vi vil teste en komponent mod den app, den er en del af, men du kan oprette integrationstests på samme måde, hvis backend er en separat tjeneste. De samme undersøgelser, vi havde i De er omskrevet i at teste den fulde integration mellem frontend og backend. Koden er lignende, men i stedet for at spøge backend-svarene, lader vi anmodningerne gå gennem til den faktiske backend. at indsætte fejl i anmodningerne. LikeButton.test.tsx PostView.integration.test.tsx chaos-fetch En vigtig detalje er, at vi skal til et URL-objekt, så det kan løse relative URL'er korrekt. I denne opsætning, JSDOM med den indfødte hentning, ville ikke engang arbejde for relative URL'er! Patches af JSDOM's For at få det til at virke. globalThis.location chaos-fetch chaos-fetch location Først og fremmest sætter vi : globalThis.location globalThis.location = new URL("http://localhost:3000/posts/1"); Så skaber vi klienter i de tests, der overskrider den indfødte hentning, og vi kan injicere latens, fejl og meget mere. chaos-fetch For den første test behøver vi ikke engang at ændre Vi overordner det kun, så det fungerer med relative URL'er: 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(); }); For den anden test opretter vi en klient, der simulerer en backendfejl for den lignende anmodning: 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, teknisk set, i den anden test, kalder vi ikke backend overhovedet, fordi Intercepter forespørgslen og returnerer en fejl, men det giver et ensartet interface til at håndtere vellykkede og mislykkede forespørgsler på samme måde. chaos-fetch Hvor denne tilgang virkelig skinner er, når du vil simulere mere komplekse netværksforhold. Eller du kan prøve, hvad der sker, hvis din backend er rate-begrænsende dig med af Middleware. throttle rateLimit En anden ting, der er svært at teste, er, om en forsinket indlæsningsspinner vises, mens anmodningen er i flyvning. Middleware til at simulere et langsomt netværk og teste, at din belastningstilstand vises korrekt. latency Hvis du ikke bekymrer dig om determinisme, kan du endda tilføje nogle tilfældige fejl for at se, hvordan din app opfører sig under uforudsigelige forhold. Konklusionen Chaos-testning er ofte forbundet med store distribuerede systemer, men det er lige så vigtigt for mindre applikationer.I denne artikel udforskede vi let chaos-drevet testning for fuldstack-apps ved hjælp af integrationstests, der bevidst bryder tingene. at indføre kaos i vores netværksforespørgsler og teste, hvordan vores applikation opfører sig under ugunstige forhold. chaos-fetch er ikke en bedre (eller værre) erstatning for MSW eller end-to-end testrammer som Playwright eller Cypress. Ved at omfavne kaos og teste, hvordan din applikation opfører sig under fejlforhold, kan du opbygge mere modstandsdygtige og robuste apps. @fetchkit/chaos-fetch @fetchkit/chaos-proxy