SavePeople () deve essere testato sull'unità
Sì, dovrebbe. Ma prova a scrivere le condizioni del test in modo indipendente dall'implementazione. Ad esempio, trasformando il tuo esempio di utilizzo in un test unitario:
function testSavePeople() {
myDataStore = new Store('some connection string', 'password');
myPeople = ['Joe', 'Maggie', 'John'];
savePeople(myDataStore, myPeople);
assert(myDataStore.containsPerson('Joe'));
assert(myDataStore.containsPerson('Maggie'));
assert(myDataStore.containsPerson('John'));
}
Questo test fa più cose:
- verifica il contratto della funzione
savePeople()
- non si preoccupa per l'implementazione di
savePeople()
- documenta l'utilizzo esemplificativo di
savePeople()
Tieni presente che puoi ancora deridere / stub / falsificare l'archivio dati. In questo caso non verificherei le chiamate di funzione esplicite, ma il risultato dell'operazione. In questo modo il mio test è preparato per futuri cambiamenti / refactor.
Ad esempio, l'implementazione dell'archivio dati potrebbe fornire un saveBulkPerson()
metodo in futuro: ora una modifica all'implementazione savePeople()
da utilizzare saveBulkPerson()
non interromperebbe il test unitario fintanto che saveBulkPerson()
funziona come previsto. E se in saveBulkPerson()
qualche modo non funziona come previsto, il test unitario lo capirà.
o tali test equivarrebbero a testare il costrutto integrato per ogni linguaggio?
Come detto, prova a verificare i risultati attesi e l'interfaccia della funzione, non per l'implementazione (a meno che non stia eseguendo test di integrazione, quindi potrebbe essere utile prendere chiamate a funzioni specifiche). Se esistono diversi modi per implementare una funzione, tutti dovrebbero funzionare con il test unitario.
Per quanto riguarda l'aggiornamento della domanda:
Test per i cambiamenti di stato! Ad esempio, parte dell'impasto verrà utilizzato. In base all'implementazione, asserisci che la quantità di usato si dough
adatta pan
o asserisce che dough
è esaurito. Asserire che pan
contiene i cookie dopo la chiamata di funzione. Asserire che oven
è vuoto / nello stesso stato di prima.
Per ulteriori test, verificare i casi limite: cosa succede se oven
non è vuoto prima della chiamata? Cosa succede se non c'è abbastanza dough
? Se pan
è già pieno?
Dovresti essere in grado di dedurre tutti i dati richiesti per questi test dagli oggetti impasto, padella e forno stessi. Non è necessario acquisire le chiamate di funzione. Tratta la funzione come se la sua implementazione non fosse disponibile per te!
In effetti, la maggior parte degli utenti TDD scrive i propri test prima di scrivere la funzione in modo da non dipendere dall'implementazione effettiva.
Per la tua ultima aggiunta:
Quando un utente crea un nuovo account, devono accadere diverse cose: 1) è necessario creare un nuovo record utente nel database 2) è necessario inviare un'e-mail di benvenuto 3) l'indirizzo IP dell'utente deve essere registrato per frode scopi.
Quindi vogliamo creare un metodo che colleghi tutti i passaggi del "nuovo utente":
function createNewUser(validatedUserData, emailService, dataStore) {
userId = dataStore.insertUserRecord(validateduserData);
emailService.sendWelcomeEmail(validatedUserData);
dataStore.recordIpAddress(userId, validatedUserData.ip);
}
Per una funzione come questa deriderei / stub / fake (qualunque cosa sembri più generale) i parametri dataStore
e emailService
. Questa funzione non esegue alcuna transizione di stato su nessun parametro da sola, ma li delega ai metodi di alcuni di essi. Vorrei provare a verificare che la chiamata alla funzione abbia fatto 4 cose:
- ha inserito un utente nell'archivio dati
- ha inviato (o almeno chiamato il metodo corrispondente) un'e-mail di benvenuto
- ha registrato l'IP dell'utente nell'archivio dati
- ha delegato qualsiasi eccezione / errore riscontrato (se presente)
I primi 3 controlli possono essere fatti con mock, stub o falsi di dataStore
e emailService
(non vuoi davvero inviare e-mail durante il test). Dal momento che ho dovuto cercare questo per alcuni dei commenti, queste sono le differenze:
- Un falso è un oggetto che si comporta come l'originale ed è in una certa misura indistinguibile. Il suo codice può normalmente essere riutilizzato attraverso i test. Questo può, ad esempio, essere un semplice database in memoria per un wrapper di database.
- Uno stub implementa solo quanto necessario per eseguire le operazioni richieste di questo test. Nella maggior parte dei casi, uno stub è specifico di un test o di un gruppo di test che richiedono solo una piccola serie di metodi dell'originale. In questo esempio, potrebbe essere uno
dataStore
che implementa solo una versione adatta di insertUserRecord()
e recordIpAddress()
.
- Un mock è un oggetto che ti consente di verificare come viene utilizzato (il più delle volte permettendoti di valutare le chiamate ai suoi metodi). Proverei a usarli con parsimonia nei test unitari poiché usandoli in realtà provi a testare l'implementazione della funzione e non l'aderenza alla sua interfaccia, ma hanno ancora i loro usi. Esistono molti framework finti per aiutarti a creare solo il modello di cui hai bisogno.
Si noti che se uno di questi metodi genera un errore, vogliamo che l'errore raggiunga il codice chiamante, in modo che possa gestire l'errore come ritiene opportuno. Se viene chiamato dal codice API, potrebbe tradurre l'errore in un codice di risposta HTTP appropriato. Se viene chiamato da un'interfaccia Web, potrebbe tradurre l'errore in un messaggio appropriato da visualizzare all'utente e così via. Il punto è che questa funzione non sa come gestire gli errori che possono essere generati.
Le eccezioni / errori previsti sono casi di test validi: si conferma che, nel caso in cui si verifichi un evento del genere, la funzione si comporta come previsto. Ciò può essere ottenuto lasciando cadere l'oggetto corrispondente finto / falso / mozzicone quando desiderato.
L'essenza della mia confusione è che per testare una tale funzione sembra necessario ripetere l'esatta implementazione nel test stesso (specificando che i metodi sono chiamati in derisione in un certo ordine) e questo sembra sbagliato.
A volte questo deve essere fatto (anche se ti interessa soprattutto questo nei test di integrazione). Più spesso, ci sono altri modi per verificare gli effetti collaterali / i cambiamenti di stato previsti.
La verifica delle chiamate esatte alle funzioni comporta test unitari piuttosto fragili: solo piccole modifiche alla funzione originale causano il fallimento. Questo può essere desiderato o meno, ma richiede una modifica ai corrispondenti test unitari ogni volta che si cambia una funzione (sia che si tratti di refactoring, ottimizzazione, correzione di bug, ...).
Purtroppo, in tal caso il test unitario perde parte della sua credibilità: poiché è stato modificato, non conferma la funzione dopo che la modifica si comporta allo stesso modo di prima.
Per un esempio, considera qualcuno che aggiunge una chiamata a oven.preheat()
(ottimizzazione!) Nell'esempio di cottura dei cookie:
- Se hai deriso l'oggetto forno, non si aspetterà quella chiamata e fallirà il test, anche se il comportamento osservabile del metodo non è cambiato (hai ancora una padella di biscotti, si spera).
- Uno stub potrebbe non funzionare, a seconda che siano stati aggiunti solo i metodi da testare o l'intera interfaccia con alcuni metodi fittizi.
- Un falso non dovrebbe fallire, dal momento che dovrebbe implementare il metodo (secondo l'interfaccia)
Nei miei test unitari, cerco di essere il più generale possibile: se l'implementazione cambia, ma il comportamento visibile (dal punto di vista del chiamante) è sempre lo stesso, i miei test dovrebbero passare. Idealmente, l'unico caso di cui ho bisogno per cambiare un test unitario esistente dovrebbe essere una correzione di bug (del test, non della funzione sotto test).