Stryker Mutator: come testare i test in JavaScript

No, non si tratta di uno scioglilingua, seppur "Testa i tuoi test" potrebbe sembrarlo. In realtà, si tratta di una tecnica che consiste nel produrre piccoli cambiamenti al codice sorgente, chiamati Mutation, e poi eseguire la suite di test per verificare se i test scritti riescono a rilevare le modifiche.

Spesso quando scriviamo dei test, la nostra più grande preoccupazione è quella di coprire quante più branch di codice possibile per poter ottenere una copertura di test di almeno 80-90%, e quando raggiungiamo questo risultato e otteniamo tutte le percentuali in verde, ci sentiamo soddisfatti dei test scritti e ne andiamo fieri 😏

Ironia a parte, non sto criticando i sistemi di coverage che rimangono comunque dei buoni strumenti di analisi per i test, ma ciò in cui voglio riporre l'attenzione è che non è tanto importante quante righe di codice sono coperte dai test, ma a quanti numeri di stati sono sottoposti ai test.

Prendiamo ad esempio una funzione che fa un calcolo su due interi, ciascuno dei quali può essere un numero tra 0 e 999.

1function test(a: number, b: number): number {
2    return a / (a + b);
3}

Questa funzione di tre righe ha 1.000.000 di stati logici, 999.999 dei quali funzioneranno correttamente e uno no (quando a + b è uguale a zero). Sapere semplicemente che una riga è stata sottoposta a test o meno non dice molto, ciò che conta è identificare tutti i possibili stati del programma. In generale questo è un problema davvero difficile.

 

Mutation Testing to the Rescue

Immaginate che per la funzione di prima vi sia un tool che si occupa di eseguire delle mutazioni al nostro codice sorgente ed esegue poi la nostra suite di test. Se la mutazione porta a un fallimento dei nostri test, vuol dire che abbiamo gestito bene lo stato, viceversa se nonostante le mutazioni i test vanno a buon fine, vuol dire che determinati stati non sono stati gestiti. Stryker Mutator si occupa proprio di questo, ovvero quello di creare per noi dei mutamenti nel nostro codice sorgente per poi verificare come i nostri test rispondono a tali mutamenti.

1function isUserOldEnough(user: User): boolean {
2  return user.age >= 18;
3}

Facciamo un esempio con la funzione sopra: Stryker troverà la dichiarazione della funzione e deciderà di modificarla in diversi modi:

1/* 1 */ return user.age > 18;
2/* 2 */ return user.age < 18;
3/* 3 */ return false;
4/* 4 */ return true;

 

Le modifiche di cui sopra vengono chiamate mutant. Dopo che i mutant sono stati trovati, vengono applicati uno per uno e i test vengono eseguiti su di essi. Se almeno uno dei test fallisce, il mutant viene sconfitto (killed). Questo è ciò che vogliamo! Se nessun test fallisce, allora è sopravvissuto (survived). Migliori sono i test, meno mutant sopravvivono.

Esistono tre tipi principali di test di mutazione:

  • Statement mutation: le dichiarazioni vengono cancellate o sostituite con una dichiarazione diversa. Ad esempio, l'istruzione A = 10 e B = 5 viene sostituita con A = 5
  • Value mutation: i valori sono modificati per trovare degli errori. Per esempio, A = 10 viene modificato con A = 5
  • Decision mutation: gli operatori artmetici e logici vengono modificati per trovare degli errori. Per esempio, A < B viene modificato con A > B

Stryker produrrà i risultati in vari formati diversi:

1  Mutant killed: /yourPath/yourFile.js: line 10:27
2  Mutator: BinaryOperator
3  -                 return user.age >= 18;
4  +                 return user.age > 18;
5
6  Mutant survived: /yourPath/yourFile.js: line 10:27
7  Mutator: RemoveConditionals
8  -                 return user.age >= 18;
9  +                 return true;

Questo utile tool può essere facilmente integrato su un progetto JavaScript / TypeScript, C# o Scala, inoltre presenta una sezione tutta dedicata all'integrazione del tool con SonarQube per potenziare ancor di più la qualità del nostro codice. Da amanti del front-end, vogliamo però soffermarci su come è stato integrato Stryker all'interno di uno dei nostri progetti in Angular, ma potete utilizzarlo anche su React, Vue o qualsiasi altro progetto JavaScript.

 

Stryker Configuration

Come prima cosa installiamo la CLI di Stryker globalmente attraverso il comando:

1npm i -g stryker-cli

A questo punto accediamo al nostro progetto da terminale:

1cd my-project

e inizializziamo Stryker tramite il comando:

1npm init stryker

Un paio di domande vi verranno poste circa il vostro progetto, quello che poi verrà creato sarà un file di configurazione per Stryker. Altra grande potenzialità è che il tool non è legato a uno specifico Framework di Test, potrete infatti utilizzarlo a prescindere se usate Jasmine, Karma, Mocha o Jest. Nel presente articolo useremo la configurazione per Jest, ma nel sito ufficiale troverete tutti i passaggi per configurare Stryker sugli altri framework di test.

Se state utilizzando Jest probabilmente il comando di inizializzazione di prima vi restituirà un errore, in questo caso vi consiglio di procedere come segue. Installate il seguente pacchetto per poter utilizzare Stryker con Jest:

1npm i --save-dev @stryker-mutator/jest-runner

successivamente occorrà creare un file di configurazione stryker.conf.js. Questa è la configurazione che ho utilizzato all'interno di un progetto Angular:

stryker.conf.js

1module.exports = {
2  $schema: './node_modules/@stryker-mutator/core/schema/stryker-schema.json',
3  mutate: [
4    'projects/**/converters/**/*.ts',
5    'projects/**/guards/*.ts',
6    'projects/**/handlers/*.ts',
7    'projects/**/interceptors/*.ts',
8    'projects/**/resolvers/*.ts',
9    'projects/**/services/*.ts',
10    'projects/**/store/effects/*.ts',
11    'projects/**/store/facades/*.ts',
12    'projects/**/store/reducers/*.ts',
13    'projects/**/store/selectors/*.ts',
14    '!projects/**/*.spec.ts'
15  ],
16  logLevel: 'error',
17  testRunner: 'jest',
18  jest: {
19    projectType: 'custom',
20    configFile: 'jest.config.json',
21    enableFindRelatedTests: true
22  },
23  incremental: true,
24  coverageAnalysis: 'off',
25  timeoutMS: 60000
26};

non ci soffermeremo su tutte le opzioni di configurazione, ma vorrei fornire alcune indicazioni su alcune parti importanti, soprattutto per migliorare le performance di esecuzione dei mutation test. La prima è la proprietà mutate, nella quale andiamo a indicare a Stryker quali file vogliamo che vengono sottoposti ai mutation test. Qui consiglio di limitarvi a indicare soltanto i file e/o le cartelle in cui sono presenti dei test, perché per esaminare tutto il codice sorgente Stryker impiega un po' di tempo, soprattutto se vi sono tanti file da esaminare. Si noti che i file con estensione .spec.ts sono stati esclusi dalle mutazioni, poiché quello che ci serve che venga mutato è il codice dell'applicativo, non il codice dei test.

Altra configurazione degna di nota è la proprietà incremental, la quale consente a Stryker di tracciare le modifiche apportate al codice e ai test ed eseguire solo i test di mutazione sul codice modificato, così da velocizzare le performance.

Una volta terminata la configurazione possiamo lanciare, dalla root di progetto, il comando:

1npx stryker run

Terminata la procedura, Stryker produrrà un file di report sia in console che all'interno della cartella reports (Consiglio: mettete sia questa cartella che la cartella .stryker-tmp all'interno del file .gitignore. Si tratta di cartelle che verranno modificate continuamente ogniqualvolta lanciate il comando precedente e che non necessitano di essere versionate)

 

Schermata da 2024-04-30 15-15-05.png

 

Schermata da 2024-04-30 15-03-03.jpg

 

Vantaggi & Svantaggi

I Mutation Test forniscono i seguenti vantaggi:

  • Aiutano a garantire l'identificazione di test / codice debole
  • Offrono un elevato livello di rilevamento degli errori
  • Incoraggia la scrittura di nuovi test unitari
  • Aiutano le organizzazioni a determinare l'utilità dei propri test attraverso l'uso di un punteggio orientato più sul test degli stati logici che sulla coverage

Tra gli svantaggi dei mutation test troviamo:

  • Non sono pratici senza l'uso di uno strumento di automazione
  • Possono richiedere molto tempo e denaro a causa dell’elevato numero di mutant da testare

 

Conclusioni

Come abbiamo visto, Stryker si dimostra uno strumento efficace nel valutare la qualità dei test, identificando aree di miglioramento e offrendo risultati chiari e utili. La sua integrazione semplice e la configurazione flessibile lo rendono un'aggiunta preziosa al processo di sviluppo, garantendo una maggiore solidità e affidabilità del codice mediante la scrittura di test più efficaci.

Contattaci.

Hai un progetto in mente?

Anche se - semplicemente - vuoi prendere un caffè con noi o vedere la nostra collezione di Action Figures scrivici tramite questo form.

Questo sito è protetto da reCAPTCHA e si applicano le Norme sulla privacy e i Termini di servizio di Google.

Ultimi Articoli