Artikel Testning · Verifiering

Property-baserad testning av AI-kod: hitta gränsfallet som exemplen missar

AI-genererad kod kommer ofta med tester för samma raka exempel som användes när funktionen skrevs. Byt frågan från “vad ska just dessa tre anrop ge?” till “vilken egenskap måste gälla för all giltig indata?” — och låt testverktyget leta efter motexemplet.

Senast granskad: 21 aug 2026

Svartvit illustration av en nätbyggd apliknande profil som betraktar en öppen mörk kub där svarta skärvor faller mot en ensam röd kvadrat.
När de genererade testfallen krymps återstår ett litet motexempel som går att förstå och spara.

Kortfattat

Ett exempeltest väljer indata och väntat resultat. Ett property-baserat test väljer i stället en generator för giltig indata och en invariant som alltid ska hålla. Verktyget provar många värden och krymper ett felande fall till ett enklare motexempel. Spara sedan det explicita motexemplet som regressionstest. Seed och sökväg hjälper dig att återskapa den ursprungliga körningen; det fasta testfallet skyddar buggen långsiktigt.

Tre gröna exempel missar samma avrundningsfel

Anta att en AI-assistent skriver en funktion som fördelar ett helt antal ören proportionellt efter positiva vikter. Den här implementationen ser rimlig ut och varje del avrundas till ett heltal:

export function fordela(total, vikter) {
  const viktSumma = vikter.reduce((summa, vikt) => summa + vikt, 0);
  return vikter.map((vikt) =>
    Math.round((total * vikt) / viktSumma)
  );
}

Tre handvalda tester går igenom: 100 öre med vikterna [1, 1] blir [50, 50], 100 med [1, 3] blir [25, 75] och 1 000 med [2, 3, 5] blir [200, 300, 500]. Exemplen delar sig rent. De provar aldrig vad som händer när flera andelar behöver avrundas samtidigt.

För fordela(1, [1, 1]) blir resultatet däremot [1, 1]. Funktionen har skapat ett extra öre. Det är ett litet motexempel mot en grundläggande regel och exakt den sorts gränsfall som kan gömma sig bakom många plausibla AI-genererade exempeltester. Den breda guiden till testning av AI-genererad kod äger teststrategin; här isolerar vi tekniken som genererar indata från en sådan regel.


Skriv domänen före invarianten

Meningen “fördelningen ska alltid bli rätt” går inte att köra. Börja med vilka värden funktionen faktiskt lovar att hantera. I exemplet är domänen:

Först därefter kommer invarianten. Resultatet ska ha lika många delar som vikter, varje del ska vara ett icke-negativt heltal och summan av delarna ska vara exakt total. Lägg märke till vad som inte står där: testet kräver inte en viss avrundningsmetod eller exakt vilken mottagare som får restöret. Det bevakar det publika kontraktet utan att låsa implementationen.

Fel invariant ger ett välfungerande feltest

En invariant är en specifikation, inte något verktyget upptäcker åt dig. Om du kräver att alla delar alltid ska vara olika kommer korrekt kod att underkännas för lika vikter. Om generatorn tillåter tomma listor trots att anroparen förbjuder dem testar du ett tillstånd som inte kan nås. Be gärna AI:n föreslå egenskaper, men låt domänregler, typer och verkliga anrop avgöra vilka som är giltiga.


Kör egenskapen med avgränsad indata och fast seed

Exemplet nedan använder fast-check tillsammans med Vitest. I fast-check består ett test av arbitraries som genererar och krymper värden, en property som beskriver egenskapen och en runner som bestämmer hur testet körs [1]. Här är alla tre delarna synliga:

import fc from 'fast-check';
import { expect, test } from 'vitest';
import { fordela } from './fordela.js';

const giltigaVikter = fc.array(
  fc.integer({ min: 1, max: 100 }),
  { minLength: 1, maxLength: 8 }
);

test('bevarar totalsumman för giltig indata', () => {
  fc.assert(
    fc.property(
      fc.integer({ min: 0, max: 1_000_000 }),
      giltigaVikter,
      (total, vikter) => {
        const delar = fordela(total, vikter);

        expect(delar).toHaveLength(vikter.length);
        expect(delar.every(Number.isInteger)).toBe(true);
        expect(delar.every((del) => del >= 0)).toBe(true);
        expect(delar.reduce((summa, del) => summa + del, 0))
          .toBe(total);
      }
    ),
    { seed: 20260821, numRuns: 500 }
  );
});

Seed-värdet gör den genererade följden repeterbar [2]. Det betyder inte att 500 körningar bevisar egenskapen för alla möjliga värden; det betyder att du har tagit 500 deterministiskt återskapbara stickprov ur den avgränsade domänen. Körningen är ett komplement till resonemanget, inte ett matematiskt bevis.

Det viktigaste kvalitetsarbetet ligger i generatorn. En heltalsgenerator med miljonspann och en kort lista av positiva vikter speglar kontraktet bättre än godtycklig JSON. Om produktionen även tillåter decimalvikter, tomma listor eller negativa justeringar ska de fallen få egna regler och egna properties. Göm inte en osäker domän bakom ett brett filter bara för att få testet grönt.


Läs det krympta motexemplet som en felrapport

När egenskapen faller rapporterar fast-check ett seed-värde, en path, motexemplet och hur många krympningssteg som gjordes. Dokumentationen visar att samma seed och path kan läggas i parametrarna till fc.assert för att gå direkt till det reducerade fallet [3]:

fc.assert(egenskapen, {
  seed: -1819918769,
  path: '0:...:3',
  endOnFailure: true
});

Siffrorna ovan är dokumentationens format, inte värden du ska kopiera. Använd seed och path från din felrapport. Path pekar körningen mot det redan krympta fallet, medan endOnFailure stoppar vid första fel i stället för att fortsätta krympa [3].

Krympningen är diagnosen. En stor slumpad lista kan få dig att misstänka ordning, overflow eller en enskild märklig vikt. Motexemplet total = 1 och vikter = [1, 1] visar i stället mekanismen: två separata avrundningar uppåt skapar mer än totalen. fast-check beskriver krympning som att ett felande värde reduceras mot ett enklare värde som fortfarande får egenskapen att falla [2].

Felsök i den här ordningen

Återskapa först med rapportens seed och path. Kontrollera sedan att det krympta fallet verkligen ligger i den avsedda domänen. Läs därefter den första brustna kontrollen: fel längd, negativ del, decimal eller fel summa. Först nu ändrar du implementationen. Om egenskapen själv är fel ska du rätta specifikationen, inte tvinga produktionskoden att uppfylla den.


Bevara värdet, inte bara slumpen

Seed och path är praktiska under felsökning, men regressionstestet ska visa buggen utan att vara beroende av hur en generator råkar traversera sina värden. Spara därför det krympta motexemplet som ett vanligt test:

test('tappar eller skapar inte ören vid lika vikter', () => {
  const delar = fordela(1, [1, 1]);
  expect(delar.reduce((summa, del) => summa + del, 0)).toBe(1);
});

Testet låser inte vilken del som får öret. Både [1, 0] och [0, 1] kan vara giltiga om kontraktet tillåter det. Det låser bara det fel som faktiskt hittades: summan får inte förändras. Behåll samtidigt property-testet. Regressionstestet skyddar det kända fallet vid varje körning; property-testet fortsätter leta efter nästa kombination du ännu inte känner till.

Det här skiljer tekniken från mutationstestning av AI-genererade tester. Mutationstestning ändrar implementationen och frågar om testsviten reagerar. Property-baserad testning ändrar indata och frågar om implementationen fortfarande uppfyller en egenskap. De kan kombineras, men de mäter inte samma sak.


När ett vanligt exempeltest är bättre

Property-baserad testning passar när många giltiga indata delar samma relation: sortering bevarar element, kodning och avkodning är varandras inverser, en parser kraschar inte för giltigt format eller en fördelning bevarar totalsumman. Ett vanligt exempeltest är tydligare när rätt resultat är en specifik produktregel.

Exakt utdata. “Guldkund med order över 500 kronor får fri frakt” uttrycks bäst med namngivna exempel på tröskeln och sidorna om den.
Liten, sluten domän. Om funktionen bara accepterar tre statusvärden kan tre tabellrader vara mer läsbara och mer fullständiga än en generator.
Dyra biverkningar. Ett test som skapar användare, debiterar en sandbox eller väntar på ett externt system behöver få noggrant valda fall, isolering och städning. Hundratals genererade körningar kan vara fel kostnadsmodell.
Känd bugg. Det krympta fallet ska alltid få ett fast regressionstest även om property-testet finns kvar.

I en kodgranskning av AI-genererad kod räcker därför tre frågor långt: vilken invariant bevakas, vilka värden räknas som giltiga och vilket explicit test sparades när property-körningen hittade ett fel? Saknas ett tydligt svar är många genererade körningar bara en större grön yta.


Källor

[1] fast-check, Core Blocks — arbitraries kombinerar generator och krympare, properties beskriver vad som ska gälla och runners styr körningen. [2] fast-check, What is Property-Based Testing? — genererad indata, upprepade körningar, seedad reproducerbarhet och krympning av motexempel. [3] fast-check, Read test reports — rapportformatet samt återkörning med seed, path och endOnFailure. Samtliga källor lästa 21 augusti 2026. Kodexemplet använder en avsiktligt felaktig avrundningsimplementation för att visa arbetsflödet; det är inte en rekommenderad fördelningsalgoritm.