Introduzione al test

Pubblicato: 2021-06-16

Benvenuti nella nostra nuova serie di blog su tutto ciò che riguarda i test . Si spera che questi post del blog ti diano un'idea generale di come li scriviamo qui su Mediatoolkit e perché. Prima di immergerci negli esempi, iniziamo con alcune definizioni, idee e vantaggi di base della scrittura di test.

Che tipo di test ci sono?

Non tutti i test sono creati uguali. Esistono diversi tipi di test per scopi diversi. Sebbene questo post si concentri principalmente sugli unit test , dovremmo essere consapevoli delle differenze e comprendere i vantaggi di ciascuno.

1. Prove manuali

Il test manuale implica test eseguiti manualmente da un tester. Questi vengono solitamente eseguiti da Quality Assurance o dagli stessi sviluppatori . Il QA dovrebbe essere visto come l'ultima linea di difesa, non il principale tester dell'applicazione (il QA che esegue test manuali come metodo principale per il test dell'interfaccia utente è una valida eccezione a questo caso).

Gli sviluppatori di solito scrivono test manuali quando vogliono eseguire alcune cose in locale e vedere come si comporta il sistema senza inviare alcun codice alla fase di staging, o che Dio ti salvi, produzione. Tuttavia, l'obiettivo di questi test non è la robustezza del codice. L'unico ruolo di questi test è catturare i bug e garantire la qualità del tuo prodotto .

2. Prove di integrazione

I sistemi sono composti da più componenti, o almeno dovrebbero esserlo. I test di integrazione controllano l'API pubblica di questi componenti. Non solo API REST, ma qualsiasi API esposta pubblicamente, come la comunicazione dell'argomento Kafka.

Se un componente "dice" emette un messaggio su un argomento in un determinato formato, ovvero la sua API pubblica o contratto. Il test di integrazione sta verificando se più componenti sviluppati individualmente possono comunicare e se i loro contratti corrispondono come gruppo.

Se si testano questi componenti solo singolarmente, il loro comportamento individuale potrebbe funzionare correttamente, ma quando si tenta di collegarli in un gruppo più ampio, si scopre che i loro contratti differiscono .

3. Test end-to-end (E2E).

Il test end-to-end garantisce il flusso dell'esperienza utente del prodotto finale . La complessità dei sistemi oggi è difficile da coprire con i test. Molti sistemi si basano l'uno sull'altro e i test E2E assicurano che il prodotto faccia ciò che ci si aspetta . Il QA convalida la correttezza del tuo prodotto esaminando il flusso degli utenti finali e controllando se tutti i sistemi si comportano come previsto.

4. Test unitari

Gli unit test sono la spina dorsale di qualsiasi software affidabile . Costituiscono la base per altri test. Testano le singole unità.

Gli sviluppatori potrebbero confondere la definizione di unità con un unico metodo. Gli unit test dovrebbero testare il comportamento di un componente che potrebbe essere composto da più classi diverse. I metodi pubblici accessibili da altri componenti devono essere testati, non testare metodi o classi protetti. Sono dettagli di implementazione, non fanno parte della tua API pubblica .

Perché dovremmo anche provare?

Se scriviamo buoni test e li scriviamo spesso, possiamo garantire la qualità del nostro prodotto prima che veda la luce del giorno.

Con l'invecchiamento del nostro sistema, i vantaggi dei test diventano sempre più evidenti. I nostri ambienti diventano affidabili, fanno risparmiare tempo di sviluppo e un sacco di fatica quando inevitabilmente le cose vanno male. I tuoi colleghi saranno grati quando potranno dare un'occhiata ai tuoi test e capire cosa dovrebbe e non dovrebbe fare il tuo codice senza dover eseguire manualmente le cose .

Codice robusto

Prima di iniziare a parlare di “codice robusto”, come dovremmo definirlo? Cosa rende il codice robusto? Ciò significa che dovremmo programmare sulla difensiva e pensare a come altri sviluppatori potrebbero abusare del nostro codice?

Il codice robusto è sempre semplice e pulito. La semplicità è robusta, la complessità è fragile . Dovremmo gestire input non validi, ma ciò non significa che dobbiamo programmare in modo difensivo e non fidarci della nostra squadra.

Legge di Gall : si scopre invariabilmente che un sistema complesso che funziona si è evoluto da un sistema semplice che funzionava.

Anche la proposizione inversa sembra essere vera: un sistema complesso progettato da zero non funziona mai e non può essere fatto funzionare. Devi ricominciare da capo, iniziando con un sistema semplice funzionante.

Refactoring sicuro

Quando il tuo codice è coperto da test, non hai più paura di modificare il codice esistente . Dopo ogni modifica, puoi eseguire i tuoi test e assicurarti di non aver rotto le cose. Quando hai dei test, non devi programmare in modo difensivo.

Il refactoring senza test sta scendendo per un pendio scivoloso che finirà in notti insonni e domeniche lavorative. Questo argomento è troppo ampio per essere trattato qui e merita un post sul blog in futuro.

Come non scriviamo i test?

È altrettanto importante sapere come NON scrivere unit test , come lo è come scriverli.

  • Scrivere un test che stampa il risultato di una chiamata al metodo non è un test poiché non convalidiamo il risultato desiderato .
  • Se il tuo test legge i dati da un file nella cartella Documenti, non è un vero unit test poiché i test non dovrebbero dipendere dall'ambiente.
  • Qualsiasi sviluppatore dovrebbe essere in grado di controllare il tuo codice ed eseguire i test con successo senza fare nient'altro.
  • Ogni test unitario dovrebbe essere indipendente da altri test . Ciò implica che anche l'ordine di esecuzione dei test non dovrebbe avere importanza.
  • L'esecuzione di test più volte dovrebbe sempre terminare con gli stessi risultati se non cambiamo alcun codice.
  • Gli unit test dovrebbero testare i comportamenti , non le singole chiamate di metodo. Non tutte le classi e i metodi devono avere il proprio test.

Il comportamento è qualcosa che produce un valore reale di cui l'utente ha bisogno. Il tuo utente ha bisogno di sapere se productFactory.create() ha creato lo stesso oggetto quando è stato chiamato due volte o se il tuo repository è stato chiamato con alcuni parametri? Probabilmente no, ma ancora molti sviluppatori scrivono esattamente questo tipo di test.

Se i tuoi test sembrano così, sono strettamente associati alla tua implementazione. Ogni volta che vuoi modificare i dettagli della tua implementazione, devi aggiornare i tuoi test, anche se il comportamento è lo stesso. I tuoi test dovrebbero cambiare solo quando cambia il comportamento, non i dettagli di implementazione . In altre parole, testa ciò che fa il tuo codice, non come lo fa.

Come scriviamo i test?

I nostri test devono seguire le migliori pratiche di codice , devono essere indipendenti dall'ambiente e devono essere eseguiti velocemente .

È importante mantenere il tempo di esecuzione del test il più breve possibile. Ogni test non dovrebbe richiedere più di un paio di millisecondi . Quando i test richiedono troppo tempo per essere eseguiti, le persone tendono a saltarli e si affidano semplicemente al loro server CI, come Jenkins, per fare storie quando non è in grado di creare i propri eseguibili di distribuzione.

Ogni test è composto da 3 sezioni "A" (il modello AAA) :

  1. Organizzare
  2. atto
  3. Asserire

1. Organizzare

Nella sezione arrangiamento del nostro test, ci assicuriamo che il nostro sistema sia in uno stato specifico prima di chiamare il comportamento che vogliamo testare . Il 'sistema' potrebbe essere un oggetto che dobbiamo impostare in un modo specifico per produrre comportamenti, creare file temporanei o cose di quella natura.

Il codice in questa sezione è generalmente più grande degli altri due combinati .

Un modello di progettazione che dovrebbe rivelarsi particolarmente utile per mantenere questa sezione piccola è Object Mother . Questo modello di progettazione è molto simile a Factory , ma ha metodi più specifici che creano oggetti preconfigurati per te. Mentre una Factory standard potrebbe avere un metodo come createCar(carDescription) , un ObjectMother avrà metodi come createRedFerrari() , createBlackTesla() o createBrokenYugo() .

2. Atto

Questa sezione del test deve avere una riga . Questa riga esegue il comportamento in prova. Se ti ritrovi a scrivere più di una riga per questa sezione, probabilmente non hai il giusto incapsulamento del tuo comportamento . I tuoi clienti non dovrebbero chiamare più metodi del tuo oggetto in un ordine particolare, quindi perché i tuoi test dovrebbero?

Questa riga è una chiamata al metodo che vogliamo testare. Se questo metodo restituisce un risultato, è necessario archiviare quel valore in una variabile per verificare se è il valore previsto nel passaggio Assert.

3. Afferma

Dopo aver preparato il sistema nella sezione Disponi ed eseguito la nostra azione sotto test nella sezione Act, dobbiamo convalidare il risultato dell'azione. Di solito controlliamo qui il risultato del metodo, ma a volte i nostri metodi non restituiscono valori, ma producono comunque effetti collaterali. Se il nostro codice doveva cambiare lo stato di un oggetto, creare un file o rimuovere qualcosa da un List , dovremmo controllare se ha fatto esattamente questo.

Stub vs Mock

La maggior parte degli sviluppatori usa i termini mock e stub in modo intercambiabile, ma ci sono delle differenze.

Uno stub non può fallire il test, un mock può farlo.

Gli stub sono basati sullo stato e restituiscono valori codificati. "Che risultato ho ottenuto?"

I mock sono basati sul comportamento , li usi per verificare come il tuo comportamento lo attraversa. "Come ho ottenuto il risultato?"

Se i tuoi unit test sono disseminati di mock, i tuoi test finiscono per essere molto fragili, il che significa che ogni volta che modifichi uno dei tuoi dettagli di implementazione, devi aggiornare tutte le tue simulazioni.

Prova solo una cosa.

Dobbiamo essere in grado di isolare un comportamento e dimostrare che funziona . Se quel comportamento dovesse funzionare in modo diverso con input diversi, dobbiamo scrivere un nuovo test per ciascuno di questi comportamenti. È difficile sapere perché il nostro test ha fallito se abbiamo un test di grandi dimensioni che verifica più cose contemporaneamente. Inoltre, diventa più difficile rimuovere le funzionalità di cui non abbiamo più bisogno e vedere quali funzionalità abbiamo interrotto quando aggiungiamo nuovo codice.

Va benissimo avere più asserzioni nella sezione finale dei tuoi test purché ciò non richieda di chiamare più volte il comportamento sottoposto a test. In tal caso, sarà difficile individuare il comportamento difettoso e risolverlo.

Quando affermiamo più comportamenti in un test, non otteniamo un quadro chiaro di cosa esattamente non funziona perché il test riporterà solo il primo errore e il resto verrà saltato. Diventa molto più difficile capire quali modifiche sono necessarie e quante cose non funzionano come previsto.

Test di denominazione

Una cosa che separa i buoni test dai grandi test è il nome del test. I test non dovrebbero solo dirci cosa fanno, ma anche quando lo fanno.

Ci sono molti buoni schemi di denominazione che puoi usare, quindi scegli quello che trovi più descrittivo e attieniti ad esso. Ecco alcuni esempi di grandi nomi di test:

  • RegistrationServiceShould.createNewAccountWhenEmailIsNotTaken
  • RegistrationServiceTest.whenEmailIsFree_createNewAccount
  • RegistrationServiceTest.if_freeEmail_when_userCreatesAccount_then_create

Quando si scrivono unit test, è molto più importante comunicare ciò che si sta testando piuttosto che seguire le migliori pratiche di denominazione dei metodi. Ad esempio, in Java, utilizziamo camelCase durante la scrittura di metodi, ma è perfettamente valido utilizzare il carattere di sottolineatura (_) per separare lo stato dall'azione nel nome del test.

Prove pulite

I test che scrivi dovrebbero seguire tutte le pratiche di codice pulito che applichi al tuo codice. I test non sono cittadini di seconda classe e devi applicare lo stesso livello di attenzione che fai con il resto del tuo codice per renderli leggibili.

La definizione della duplicazione del codice nei test è molto importante. Il principio DRY (Don't Repeat Yourself) si applica all'estrazione di comportamenti che cambiano per lo stesso motivo. I test cambiano per motivi diversi, quindi sii vario nell'estrarre cose dai tuoi test se davvero non cambiano per lo stesso motivo. Avviso spoiler, spesso non lo fanno.

if le affermazioni non appartengono ai test. L'istruzione if ci dice che il nostro test fa almeno due cose diverse e sarebbe meglio se riscrivessimo il nostro test come due test diversi. Se nominati correttamente, sarà più facile capire cosa fanno i test e quali sono tutti i diversi comportamenti.

Quando dovremmo scrivere i test?

Guidati dai principi del TDD, dovremmo scrivere dei test prima di scrivere un nuovo codice .

Quando dobbiamo aggiungere una nuova funzionalità, descriviamo prima il comportamento desiderato come un nuovo test. Apportiamo il minor numero di modifiche necessarie per superare quel test senza interromperne altri.

Altrimenti, più passa il tempo, più codice abbiamo che non è stato testato e aumenta la possibilità di introdurre bug o overengineering .

Inoltre, i test diventano più complessi poiché ora abbiamo più codice da testare e ciò che di solito accade è che gli sviluppatori adattano i test al codice. Ciò significa che regoliamo il comportamento in modo che corrisponda a ciò che fa il nostro codice invece del contrario.

Quando scriviamo i test in anticipo, la definizione del problema diventa più piccola ed è più facile avvolgere la testa attorno a tali problemi rispetto a comportamenti più generici e complessi.

Pensieri finali

Sebbene la scrittura di test possa sembrare una cosa facoltativa da fare, è fondamentale iniziare con buone basi. La codifica è già difficile, semplifica la vita a te stesso e ai tuoi compagni di squadra scrivendo codice testabile che sia più facile da leggere, comprendere e mantenere . Infine, se hai difficoltà a rendere i tuoi test conformi ai tuoi desideri, il problema è più probabile nel tuo codice che nei tuoi test.

Interessato a lavorare su un codice pulito e testabile?
Dai un'occhiata alla nostra posizione aperta per Senior Backend Developer
oppure inviaci una candidatura aperta !