(Acest articol a fost publicat pentru prima dată pe jakub::sobolewskiși cu amabilitate a contribuit la R-bloggeri). (Puteți raporta problema legată de conținutul acestei pagini aici)
Doriți să vă distribuiți conținutul pe R-bloggeri? dați clic aici dacă aveți un blog, sau aici dacă nu aveți.
Testele tale trec. Acoperirea este mare. Totul arată în regulă – până când cineva găsește o eroare în producție pe care nu ai prins-o – totul din cauza unei afirmații proaste.
Acoperirea codului vă spune ce linii au rulat. Nu spune nimic despre dacă acele linii sunt de fapt testate. Puteți șterge fiecare afirmație din suita dvs. de testare, rulați covrși încă vezi 100%. Acoperirea este o măsură de execuție, nu corectitudine. Acest decalaj este exact ceea ce {muttest} a fost construit pentru a se închide – iar 0.2.0 îl face mult mai capabil decât versiunea anterioară.
📝 Vezi jurnalul complet de modificări aici.
Ce este testarea mutațiilor?
Testarea mutațiilor pune o întrebare mai dificilă decât acoperirea: dacă acest cod ar fi greșit subtil, testele dvs. ar observa?
Funcționează făcând modificări mici, deliberate, la codul sursă – schimb > pentru >=răsturnând TRUE la FALSEînlocuind && cu || — și apoi rulați suita de teste pentru fiecare versiune modificată. Fiecare versiune modificată se numește a mutant. Dacă testele tale eșuează, mutantul este ucis: testele dvs. au observat schimbarea. Dacă testele tale trec, mutantul supravietuit: testele tale sunt orb la acest tip de bug.
Rezultatul este a scorul de mutație:
- 0% — Testele tale trec indiferent de ce face codul. Afirmațiile lipsesc sau sunt banale.
- 100% — Fiecare mutație declanșează un eșec al testului. Testele tale sunt strânse.
Spre deosebire de acoperire, acest scor reflectă calitatea afirmaţieinu doar execuție. O suită de teste plină de expect_true(is.numeric(x)) controalele vor atinge o acoperire de 100% în timp ce scad orice eșec semnificativ. Testarea mutațiilor dezvăluie asta.
De ce ar trebui să-ți pese
Iată exemplul canonic. Funcția is_adult are o condiție la limită:
# R/is_adult.R
is_adult <- function(age) {
age >= 18
}
Și aceste teste oferă o acoperire de 100%:
# tests/testthat/test-is_adult.R
test_that("is_adult returns TRUE for adults", {
expect_true(is_adult(25))
})
test_that("is_adult returns FALSE for minors", {
expect_false(is_adult(10))
})
Ambele teste trec. Ambele ar trece în continuare dacă >= au fost înlocuite accidental cu >. Valoarea limită 18 nu este niciodată testat, deci niciun mutant nu este ucis:
#' R/is_adult.R — mutant 1: ">=" → ">"
is_adult <- function(age) {
age > 18
}
Imaginați-vă că acest bug ajunge în producție. Un utilizator de 18 ani încearcă să se înscrie, iar sistemul îi respinge. Bug-ul este real, dar testele tale nu au văzut-o niciodată venind.
Funcţionare muttest expune imediat asta:
library(muttest) plan <- muttest_plan( mutators = comparison_operators() ) muttest(plan)
Tabelul de progres arată un supraviețuitor. Remedierea este un singur test:
test_that("is_adult returns TRUE at the boundary age", {
expect_true(is_adult(18)) # kills the >= → > mutant
})
Acest mutant supraviețuitor nu este o problemă de rezolvat – este o specificație pe care ați uitat să o scrieți.
Problema testului LLM
Mulți dezvoltatori folosesc acum LLM-uri pentru a genera teste. Oricum, cui îi place să scrie singur teste?
LLM-urile sunt rapide și produc cod corect din punct de vedere sintactic, dar pot produce cazuri evidente, pot pierde limite sau doar testează proprietățile codului. The is_adult suita de teste de mai sus este ceea ce ar putea produce un model de limbă: bine din punct de vedere structural, incomplet din punct de vedere semantic.
Testarea mutațiilor vă oferă un semnal obiectiv despre cât de puternice sunt de fapt testele, indiferent dacă le-ați scris singur sau au fost generate de un LLM. Un scor de mutație scăzut nu înseamnă că LLM a făcut o treabă proastă – înseamnă că acum știi exact unde să întăriți afirmațiile. Testele generate de LLM au nevoie de validare externă la fel de mult ca testele scrise de oameni.
muttest oferă instrumente pentru a ajuta la această validare.
Ce este nou în 0.2.0
Biblioteca extinsă de mutatori
Cea mai mare adăugare a acestei versiuni este o listă completă de mutatori noi, organizați în mutatori individuali și colecții prestabilite gata făcute.
Noi mutatori individuali:
boolean_literal("TRUE", "FALSE")— inversează constantele booleene:TRUE → FALSEna_literal("NA", "NULL")— schimbă variantele NA și NULL:NA → NULLnegate_condition()— împachetăriifconditii in!(...):if (x > 0)→if (!(x > 0))remove_condition_negation()— fâșii care conduc!din conditii:if (!done)→if (done)numeric_increment()/numeric_decrement()— deplasează constantele numerice cu una:5 → 6,5 → 4index_increment()/index_decrement()— schimbă indici de indice:x(i)→x(i + 1L)string_empty()— înlocuiește șirurile nevide cu"":"hello" → ""string_fill()— înlocuiește șirurile goale cu"mutant":"" → "mutant"call_name("any", "all")— schimbă numele funcțiilor:any(x) → all(x)remove_negation()— îndepărtează!oriunde:!is.na(x) → is.na(x)replace_return_value()— înlocuiește valorile returnate explicite cuNULL:return(x) → return(NULL)delete_statement()— elimină sarcinile și apelurile de sine stătătoare pe rând, observând efectele secundare netestate și sarcinile moarte
Colecții noi prestabilite — treceți un singur apel și obțineți setul complet de mutatori relevanți:
boolean_literals()—TRUE ↔ FALSE,T ↔ Fna_literals()—NA ↔ NULL,NA ↔ NA_real_,NA ↔ NA_integer_,NA ↔ NA_character_numeric_literals()— combinenumeric_increment()şinumeric_decrement()index_mutations()— combineindex_increment()şiindex_decrement()string_literals()— combinestring_empty()şistring_fill()condition_mutations()— combinenegate_condition()şiremove_condition_negation()
Cele trei presetări ale operatorului de la 0.1.0 sunt încă acolo — arithmetic_operators(), comparison_operators(), logical_operators() — și acum au companie.
O configurație de pornire practică acoperă cea mai mare parte a ceea ce ați dori să prindeți în logica de afaceri:
plan <- muttest_plan(
source_files = "R/my_file.R",
mutators = c(
arithmetic_operators(),
comparison_operators(),
logical_operators(),
condition_mutations(),
numeric_literals(),
list(remove_negation())
)
)
Strat în boolean_literals(), na_literals(), string_literals()sau index_mutations() pe baza a ceea ce face codul tău de fapt.
Mutatorii sunt acum parametrizați
Mutatorii individuali acceptă argumentele de configurare. operator("+", "-") şi boolean_literal("TRUE", "FALSE") vă permit să definiți exact ce simbol să înlocuiți și cu ce – astfel încât să puteți exprima mutațiile care contează pentru domeniul dvs. fără a scrie un mutator personalizat de la zero. The Mutator clasa de bază este acum exportată și pentru cazurile în care doriți să mergeți mai departe și să construiți un mutator complet personalizat.
Mutanții supraviețuiți sunt acum raportați
The ProgressMutationReporter a arătat anterior doar numărul de mutanți uciși și total. În 0.2.0, acum raportează au supraviețuit mutanților — cele pe care le-a ratat testele tale.
Acesta este semnalul care contează. Supraviețuitorii nu sunt zgomot; fiecare reprezintă un gol real în suita dvs. de teste. Văzându-le apărute direct în rezultatul de progres face bucla de feedback mai strânsă: rulați muttestcitește supraviețuitorii, adaugă un test, repetă.
i Mutation Testing | K | S | E | T | % | Mutator | File v | 1 | 0 | 0 | 1 | 100 | > → < | shipping.R x | 1 | 1 | 0 | 2 | 50 | > → >= | shipping.R -- Survived Mutants ----------------------------------------------- shipping.R > → >= 2- if (weight_kg > 5) 15.00 else 5.00 2+ if (weight_kg >= 5) 15.00 else 5.00 -- Results -------------------------------------------------------- ( KILLED 1 | SURVIVED 1 | ERRORS 0 | TOTAL 2 | SCORE 50.0% )
Timeouts și îmbunătățirea gestionării erorilor
Testarea mutațiilor funcționează prin rularea suita de teste o dată pentru fiecare mutant. Unele mutații produc cod care se blochează – o buclă infinită, un apel de blocare, un calcul care nu se finalizează niciodată. În 0.1.0, asta ți-ar bloca întreaga rulare.
În 0.2.0, muttest() suporturi timeout-uri per-mutant. Setați un timeout și orice mutant al cărui test o depășește este marcat ca eroat. Restul cursei continuă neafectat.
Gestionarea erorilor în general a fost îmbunătățită. Atunci când execuția testului eșuează în mod neașteptat, erorile sunt acum capturate și raportate curat, mai degrabă decât să apară ca condiții nerezolvate care opresc întreaga rulare. Acest lucru face testarea mutațiilor mai robustă în proiectele reale în care mediile de testare nu sunt întotdeauna perfect controlate.
Execuție paralelă
Versiunea 0.1.0 a rulat mutanți secvenţial. În fișierele mari cu mulți mutanți, asta se adaugă. muttest() suportă acum executie paralela cu {mirai} sub capotă: mutanții pot fi rulați simultan pe mai mulți lucrători, reducând timpul de rulare în depozite mai mari.
Noțiuni de bază
Instalați din CRAN:
install.packages("muttest")
Alegeți un fișier cu o logică semnificativă – ramificare, comparații, aritmetică. Definiți un plan:
library(muttest) plan <- muttest_plan( source_files = "R/your_file.R", mutators = comparison_operators() ) muttest(plan)
Citiți rezultatul. Găsiți supraviețuitorii. Adaugă testele pe care le implică. Repeta.
Începeți cu un fișier și o presetare mutator. Urmărește o îmbunătățire semnificativă a scorului la fiecare iterație, mai degrabă decât urmărirea 100% imediat. Un scor de peste 80% la logica critică de afaceri este o țintă puternică de pornire.
Încercați-l pe un fișier în care bănuiți că testele sunt slabe. Supraviețuitorii vă vor spune exact ce să adăugați.
Mi-ar plăcea să aud de la tine
Solicitările de funcții sunt la fel de binevenite. Dacă există un fel de modificare a codului pentru care doriți să testați și nu există încă o modalitate bună de a o exprima, vă rugăm să aruncați o problemă în depozit.
