Vi raderade 37 tester och vår täckning blev bättre

Erik Treviño avatar
Erik Treviño
Cover for Vi raderade 37 tester och vår täckning blev bättre

Vi raderade 37 tester förra månaden.

Vår täckning blev bättre.

Inte “förblev densamma” — blev bättre. Sviten körde snabbare, flakade mindre, och de WORKFLOW-tester som blev kvar täckte redan varje väg som de raderade testerna kontrollerade. Vi förlorade inte en enda meningsfull assertion. Vi blev av med dödvikt.

Det här är något de flesta team inte vill erkänna: en testsvit som bara växer är som en kodbas som bara växer. Till slut överstiger underhållskostnaden värdet. Varje team har instinkten “låt oss lägga till fler tester”. Nästan inget team har disciplinen “låt oss granska det vi redan har”.

Klassificeringssystemet med fyra etiketter

Jag byggde en testklassificerare. Inte ett AI-verktyg — en taxonomi. Ett systematiskt sätt att titta på varje test i en svit och besvara en fråga: Förtjänar det här testet sin plats?

Varje test får en av fyra etiketter:

  • WORKFLOW — Testar en riktig användarresa från början till slut. Inloggning → navigera → utför handling → verifiera resultat. Dessa är ryggraden. Behåll.
  • FLUFF — Verifierar något som redan täcks av ett annat test i ett bättre lager. Valideringstestet för inloggningsformuläret som också finns som enhetstest. Ta bort.
  • MERGE — Två tester som delar 80 % av sin setup och sina assertions men testar något olika grenar. Kombinera till ett parametriserat test.
  • KEEP_DETAILED — Ett gränsfall som genuint spelar roll (tidszonshantering, behörighetsgränser, datamigrationsvägar). Behåll, men utvärdera om det hör hemma i E2E-lagret eller kan flyttas till integration/enhetstest.

Taxonomin är avsiktligt enkel. Fyra etiketter. Ingen ambiguitetsskala. Ingen “kanske”-kategori. Varje test får en definitiv klassificering.

Hur varje etikett ser ut i praktiken

WORKFLOW: en riktig användarresa

// WORKFLOW — This test earns its place. It validates a complete user journey
// that can only be tested through a browser.
test('user creates a report and shares it with a teammate', async ({ page }) => {
  await page.goto('/reports');
  await page.getByRole('button', { name: 'New Report' }).click();

  // Fill out the report form
  await page.getByLabel('Report Name').fill('Q1 Sales Summary');
  await page.getByRole('combobox', { name: 'Template' }).selectOption('quarterly');
  await page.getByRole('button', { name: 'Generate' }).click();

  // Wait for the report to generate (involves backend processing)
  await page.waitForResponse(resp =>
    resp.url().includes('/api/reports') && resp.status() === 201
  );
  await expect(page.getByText('Q1 Sales Summary')).toBeVisible();

  // Share with a teammate
  await page.getByRole('button', { name: 'Share' }).click();
  await page.getByLabel('Email').fill('teammate@company.com');
  await page.getByRole('button', { name: 'Send' }).click();
  await expect(page.getByText('Report shared successfully')).toBeVisible();
});

Det här testet utövar navigering, formulärinskickning, backendbearbetning, rendering och en sekundär handling. Det kan bara köras i en webbläsare. Det validerar en riktig användarberättelse. WORKFLOW.

FLUFF: redan täckt på annat håll

// FLUFF — This test validates form validation rules through the browser.
// The exact same validation logic is covered by unit tests on the
// validation schema. The E2E test adds 45 seconds of browser execution
// to verify something the unit test covers in 12 milliseconds.
test('report name field shows error when empty', async ({ page }) => {
  await page.goto('/reports');
  await page.getByRole('button', { name: 'New Report' }).click();
  await page.getByLabel('Report Name').fill('');
  await page.getByLabel('Report Name').blur();
  await expect(page.getByText('Report name is required')).toBeVisible();
});

Det här testet är inte fel. Assertionen är giltig. Men WORKFLOW-testet ovan navigerar redan till det här formuläret. Om valideringen vore trasig skulle workflow-testet misslyckas vid formulärinskickningssteget. FLUFF-testet lägger till 45 sekunders exekveringstid för att åter-verifiera en valideringsregel som ett enhetstest täcker på millisekunder.

MERGE: två tester som borde vara ett

// MERGE candidate A
test('admin can delete a report', async ({ page }) => {
  await loginAs(page, 'admin');
  await page.goto('/reports');
  await page.getByRole('row', { name: 'Q1 Sales' }).getByRole('button', { name: 'Delete' }).click();
  await page.getByRole('button', { name: 'Confirm' }).click();
  await expect(page.getByRole('row', { name: 'Q1 Sales' })).not.toBeVisible();
});

// MERGE candidate B — 90% identical setup, just checks a different element
test('admin sees delete confirmation dialog', async ({ page }) => {
  await loginAs(page, 'admin');
  await page.goto('/reports');
  await page.getByRole('row', { name: 'Q1 Sales' }).getByRole('button', { name: 'Delete' }).click();
  await expect(page.getByRole('dialog', { name: 'Confirm deletion' })).toBeVisible();
});

Dessa två tester delar samma setup, samma navigering, samma roll och samma initiala handling. Test B är en delmängd av test A. Slå ihop dem — det första testet klickar redan igenom bekräftelsedialogen och verifierar raderingen.

Granskningsresultaten

Jag tillämpade den här taxonomin på en E2E-svit med 180 tester. Inte i en enda genomgång — tre omgångar av klassificering med manuell granskning i varje steg.

EtikettAntalProcentÅtgärd
WORKFLOW13474 %Behåll — dessa är sviten
FLUFF3721 %Ta bort — täcks av andra lager eller av workflow-tester
MERGE21 %Kombinera till 1 test vardera
KEEP_DETAILED74 %Behåll i E2E, eller migrera till integrationslagret

21 % av sviten var FLUFF. Vart femte test förbrukade CI-tid, bidrog till flake-frekvensen och krävde underhåll — samtidigt som det gav noll unik täckning.

Granskningscykeln i tre omgångar

En klassificering i en enda genomgång är inte pålitlig. Jag missade klassificeringar i första omgången, fångade dem i den andra och förfinade taxonomireglerna i den tredje.

Omgång 1: initial klassificering. Tillämpa de fyra etiketterna på varje testfil. Använd statisk analys för att flagga tester som inte innehåller navigeringshandlingar (troligen FLUFF), tester som delar 80 %+ av sina locators med ett annat test (troligen MERGE) och tester som bara verifierar ett enda element (troligen FLUFF eller KEEP_DETAILED).

# Quick static analysis: find tests that never call page.goto() or navigate
# These are often fluff tests that rely on another test's setup
grep -rL "page\.goto\|page\.click.*nav\|page\.getByRole.*link" tests/e2e/*.spec.ts

Omgång 2: manuell granskning av varje FLUFF-etikett. För varje test som etiketteras FLUFF, besvara två frågor: (1) Finns det ett WORKFLOW-test som redan täcker den här vägen? (2) Verifierar ett enhets- eller integrationstest redan samma beteende? Om båda svaren är ja håller FLUFF-etiketten. Om något är nej, omklassificera.

Omgång 3: förfina och omgranska. Uppdatera klassificeringsreglerna baserat på vad du lärde dig i omgång 2. Kör om den statiska analysen. Kontrollera falskt negativa — tester du etiketterade som WORKFLOW som egentligen borde vara MERGE eller KEEP_DETAILED.

Tre omgångar. Inte en. Kostnaden för en falskt positiv (att radera ett test som ger unik täckning) är mycket högre än kostnaden för en extra granskningsomgång.

Före och efter

Svitens prestanda

MätetalFöre (180 tester)Efter (141 tester)Förändring
Total svitlängd24 min 12 sek17 min 45 sek-27 %
Flaky test-frekvens3,2 % av körningarna1,1 % av körningarna-66 %
Unik workflow-täckning134 vägar134 vägarIngen förändring
Veckounderhållstimmar~4 tim~2,5 tim-38 %

Nedgången i flake-frekvens är det mest talande mätetalet. De 37 borttagna testerna hade den högsta flake-frekvensen i sviten — eftersom de testade implementationsdetaljer genom det mest ömtåliga lagret möjligt. En formulärvalideringsregel testad genom en webbläsare är känslig för renderingstiming, DOM mutation observers och fokushantering. Samma regel testad i ett enhetstest har noll flake-yta.

Vad vi inte förlorade

Det här är siffran som spelar roll: 134 workflow-vägar före, 134 workflow-vägar efter. Varje riktig användarresa som testades före granskningen testas fortfarande efter. Vi förlorade inte täckning — vi förlorade redundans.

Distinktionen spelar roll. Täckning som finns i två lager (enhetstest + E2E) är inte “mer täckning” än täckning som finns i ett korrekt lager. Det är duplicerad kostnad med samma skydd.

Varför FLUFF-tester ackumuleras

FLUFF-tester skapas inte av slarviga ingenjörer. De ackumuleras av strukturella skäl:

  1. Saknade testlager. När din kodbas inte har ett komponenttestlager eller ett API-integrationstestlager blir E2E dumpningsplatsen. Varje beteendekontroll, varje gränsfall, varje “låt mig bara verifiera den här saken” hamnar i E2E för att det inte finns någon annanstans att lägga det.

  2. Antagandet “fler tester = bättre”. Teamets velocitymetriker som räknar tillagda tester per sprint uppmuntrar kvantitet framför arkitektur. Ingen får erkännande för att radera ett test, även när raderingen förbättrar sviten.

  3. Kopiera-klistra-testskapande. En utvecklare kopierar ett befintligt E2E-test som mall, ändrar assertionen, och nu finns det två tester som delar 90 % av sin setup. Inget av dem är fel individuellt. Tillsammans är de en MERGE-kandidat.

  4. Rädsla för att ta bort tester. “Tänk om vi behöver det senare?” Det här är testvärldens motsvarighet till samlarmani. Om täckningen finns i ett annat lager tar borttagning av E2E-testet inte bort skyddsnätet — det tar bort dubbletten.

Granskningsprotokollet: vad du kan göra på måndag morgon

  1. Exportera din testfillista. Varje E2E-testfil i din svit, en per rad.

  2. Tagga varje test med dess primära assertion. Vad kontrollerar det här testet egentligen? “Användaren kan logga in.” “Felmeddelande visas vid ogiltig inmatning.” “Data laddas efter navigering.”

  3. Korskontrollera mot dina enhets-/integrationstester. För varje E2E-assertion, täcker redan ett snabbare test samma beteende?

  4. Tillämpa de fyra etiketterna. WORKFLOW, FLUFF, MERGE, KEEP_DETAILED.

  5. Granska FLUFF-etiketter med teamet. Radera inte ensidigt. Visa teamet vilka tester du föreslår att ta bort och vilka workflow-tester som redan täcker dessa vägar.

  6. Radera i en enda PR med före/efter-metriker. Kör hela sviten före och efter. Mät varaktighet, flake-frekvens och unika täckta vägar. Siffrorna bör tala för sig själva.

Konsten att subtrahera

Varje ingenjör vet hur man lägger till ett test. Skriv setupen, skriv assertionen, se det passera, committa. Det känns produktivt. Testantalet går upp. Täckningsrapporten visar grönt.

Att ta bort ett test kräver en annan kompetensuppsättning. Du måste förstå vad testet faktiskt täcker (inte vad dess namn säger), om den täckningen finns någon annanstans i sviten, om assertionen hör hemma i det här lagret, och om borttagning skapar en lucka eller bara eliminerar en dubblett.

Det är svårare. Det kräver förståelse för hela testarkitekturen, inte bara den enskilda testfilen. Det kräver tillit till att din kvarvarande svit är tillräcklig — tillit grundad i analys, inte hopp.

En smal, workflow-fokuserad svit med 141 tester som körs på 17 minuter är mer värdefull än en uppblåst svit med 180 tester som körs på 24 minuter och flakar dubbelt så ofta. Den mindre sviten är snabbare att köra, billigare att underhålla, mer tillförlitlig att tolka, och täcker exakt samma användarresor.

Att lägga till tester är enkelt. Att ta bort tester är ingenjörskonst.