Dovresti codificare i tuoi dati su tutti i unit test?


33

La maggior parte dei tutorial / esempi di unit test là fuori di solito comporta la definizione dei dati da testare per ogni singolo test. Immagino che questo faccia parte della teoria "tutto dovrebbe essere testato in modo isolato".

Tuttavia, ho scoperto che quando si ha a che fare con applicazioni a più livelli con molti DI , il codice richiesto per l'impostazione di ogni test è molto lungo. Invece ho creato un certo numero di classi di basi di test che ora posso ereditare e che hanno un sacco di ponteggi di prova pre-costruiti.

Come parte di questo, sto anche creando set di dati falsi che rappresentano il database di un'applicazione in esecuzione, anche se di solito solo una o due righe in ogni "tabella".

È una pratica accettata predefinire, se non tutti, la maggior parte dei dati dei test in tutti i test unitari?

Aggiornare

Dai commenti qui sotto sembra che stia facendo più integrazione dei test unitari.

Il mio progetto attuale è ASP.NET MVC, utilizzando Unit of Work over Entity Framework Code First e Moq per i test. Ho deriso UoW e i repository, ma sto usando le vere classi di logica aziendale e testando le azioni del controller. I test controlleranno spesso che la UoW sia stata impegnata, ad esempio:

[TestClass]
public class SetupControllerTests : SetupControllerTestBase {
  [TestMethod]
  public void UserInvite_ExistingUser_DoesntInsertNewUser() {
    // Arrange
    var model = new Mandy.App.Models.Setup.UserInvite() {
      Email = userData.First().Email
    };

    // Act
    setupController.UserInvite(model);

    // Assert
    mockUserSet.Verify(m => m.Add(It.IsAny<UserProfile>()), Times.Never);
    mockUnitOfWork.Verify(m => m.Commit(), Times.Once);
  }
}

SetupControllerTestBasesta costruendo il finto UoW e sta istanziando il userLogic.

Molti test richiedono di avere un utente o un prodotto esistente nel database, quindi ho precompilato ciò che restituisce la finta UoW, in questo esempio userData, che è solo un IList<User>con un singolo record utente.


4
Il problema con tutorial / esempi è che devono essere semplici, ma non è possibile mostrare la soluzione a un problema complesso su un semplice esempio. Dovrebbero essere accompagnati da "casi studio" che descrivono come lo strumento viene utilizzato in progetti reali di dimensioni ragionevoli, ma raramente lo sono.
Jan Hudec,

Forse potresti aggiungere alcuni piccoli esempi di codice di cui non sei totalmente soddisfatto.
Luc Franken,

Se è necessario molto codice di installazione per eseguire un test, si rischia di eseguire un test funzionale. Se il test fallisce quando si cambia codice ma non c'è nulla di sbagliato nel codice. È sicuramente un test funzionale.
Reactgular,

Il libro "xUnit Test Patterns" è un valido esempio per dispositivi riutilizzabili e aiutanti. Il codice di prova dovrebbe essere gestibile come qualsiasi altro codice.
Chuck Krutsinger,

Questo articolo può essere utile: yegor256.com/2015/05/25/unit-test-scaffolding.html
yegor256

Risposte:


25

Alla fine, vuoi scrivere il minor codice possibile per ottenere il maggior numero di risultati possibile. Avere molto dello stesso codice in più test a) tende a provocare la codifica copia-incolla eb) significa che se una firma del metodo cambia, si può finire per dover riparare molti test non funzionanti.

Uso l'approccio di avere classi TestHelper standard che mi forniscono molti tipi di dati che uso abitualmente, in modo da poter creare serie di entità standard o classi DTO per i miei test da interrogare e sapere esattamente cosa otterrò ogni volta. Quindi posso chiamare TestHelper.GetFooRange( 0, 100 )per ottenere un intervallo di 100 oggetti Foo con tutte le loro classi / campi dipendenti impostati.

Soprattutto dove ci sono relazioni complesse configurate in un sistema di tipo ORM che devono essere presenti affinché le cose funzionino correttamente, ma non sono necessariamente significative per questo test che possono far risparmiare molto tempo.

In situazioni in cui sto testando vicino al livello dei dati, a volte creo una versione di prova della mia classe di repository che può essere interrogata in un modo simile (di nuovo questo è in un ambiente di tipo ORM e non sarebbe rilevante rispetto a un database reale), perché deridere le risposte esatte alle query richiede molto lavoro e spesso offre solo vantaggi minori.

Ci sono alcune cose da fare attenzione, anche se nei test unitari:

  • Assicurati che le tue beffe siano beffe . Le classi che eseguono operazioni intorno alla classe da testare devono essere oggetti simulati se si eseguono test di unità. Le tue classi DTO / tipo di entità possono essere la cosa reale, ma se le classi eseguono operazioni devi prenderle in giro, altrimenti quando il codice di supporto cambia e i tuoi test iniziano a fallire, devi cercare molto più a lungo per capire quale cambiamento effettivamente causato il problema.
  • Assicurati di testare le tue lezioni . A volte, se si guarda attraverso una serie di unit test, diventa evidente che metà dei test sta effettivamente testando il framework beffardo più del codice reale che dovrebbero testare.
  • Non riutilizzare oggetti finti / di supporto Questo è un grosso problema: quando si inizia a provare a diventare intelligenti con i test unitari che supportano il codice, è davvero facile creare inavvertitamente oggetti che persistono tra i test, il che può avere effetti imprevedibili. Ad esempio, ieri ho avuto un test che è stato superato quando eseguito da solo, superato quando sono stati eseguiti tutti i test nella classe, ma non è riuscito quando è stata eseguita l'intera suite di test. Si è scoperto che c'era un subdolo oggetto statico lontano in un aiutante di prova che, quando l'ho creato, non avrebbe mai causato problemi. Ricorda: all'inizio del test, tutto viene creato, alla fine del test tutto viene distrutto.

10

Qualunque cosa renda più leggibile l'intento del test.

Come regola generale:

Se i dati fanno parte del test (ad es. Non devono stampare righe con uno stato 7), codificali nel test, in modo che sia chiaro ciò che l'autore intendeva accadere.

Se i dati sono solo di riempimento per assicurarsi che abbia qualcosa con cui lavorare (ad es. Non dovrebbe contrassegnare il record come completo se il servizio di elaborazione genera un'eccezione), quindi avere un metodo BuildDummyData o una classe di test che mantenga i dati irrilevanti fuori dal test .

Ma nota che sto lottando per pensare a un buon esempio di quest'ultimo. Se ne hai molti di questi in un dispositivo di unit test, probabilmente hai un problema diverso da risolvere ... forse il metodo sotto test è troppo complesso.


+1 Sono d'accordo. Questo puzza come quello che sta testando è strettamente accoppiato per i test unitari.
Reactgular,

5

Diversi metodi di test

Per prima cosa, definisci cosa stai facendo: unit test o test di integrazione . Il numero di livelli è irrilevante per i test unitari poiché è molto probabile che si verifichi solo una classe. Il resto lo prendi in giro. Per i test di integrazione è inevitabile testare più livelli. Se disponi di buoni test unitari, il trucco è rendere i test di integrazione non troppo complessi.

Se i test unitari sono validi, non è necessario ripetere i test di tutti i dettagli quando si eseguono i test di integrazione.

I termini che utilizziamo dipendono leggermente dalla piattaforma, ma puoi trovarli in quasi tutte le piattaforme di test / sviluppo:

Esempio di applicazione

A seconda della tecnologia utilizzata, i nomi potrebbero differire, ma lo userò come esempio:

Se si dispone di una semplice applicazione CRUD con modello Prodotto, ProductsController e una vista indice che genera una tabella HTML con prodotti:

Il risultato finale dell'applicazione mostra una tabella HTML con un elenco di tutti i prodotti attivi.

Test unitari

Modello

Il modello che puoi testare abbastanza facilmente. Esistono diversi metodi per farlo; usiamo infissi. Penso che sia quello che tu chiami "set di dati falsi". Quindi, prima di eseguire ogni test, creiamo la tabella e inseriamo i dati originali. La maggior parte delle piattaforme ha metodi per questo. Ad esempio, nella classe di test, un metodo setUp () che viene eseguito prima di ogni test.

Quindi eseguiamo il nostro test, ad esempio: testGetAllActive products.

Quindi testiamo direttamente su un database di test. Non deridiamo l'origine dati; lo facciamo sempre lo stesso. Questo ci consente ad esempio di testare con una nuova versione del database, e sorgeranno eventuali problemi di query.

Nel mondo reale non puoi sempre seguire la responsabilità singola al 100% . Se vuoi farlo ancora meglio, potresti usare un'origine dati che prendi in giro. Per noi (usiamo un ORM) che sembra testare la tecnologia già esistente. Inoltre, i test diventano molto più complessi e in realtà non verificano le query. Quindi continuiamo così.

I dati hard coded vengono memorizzati separatamente nelle fixture. Quindi il dispositivo è come un file SQL con un'istruzione create table e inserisce i record che usiamo. Li manteniamo piccoli a meno che non ci sia una reale necessità di testare con molti record.

class ProductModel {
  public function getAllActive() {
    return $this->find('all', array('conditions' => array('active' => 1)));
  }
}

controllore

Il controller ha bisogno di più lavoro, perché non vogliamo testare il modello con esso. Quindi quello che facciamo è deridere il modello. Ciò significa: testiamo il metodo index () che dovrebbe restituire un elenco di record.

Quindi deridiamo il metodo del modello getAllActive () e aggiungiamo dati fissi in esso (ad esempio due record). Ora testiamo i dati che il controller invia alla vista e confrontiamo se recuperiamo davvero quei due record.

function testProductIndexLoggedIn() {
  $this->setLoggedIn();
  $this->ProductsController->mock('ProductModel', 'index', function(return array(your records) ));
  $result=$this->ProductsController->index();
  $this->assertEquals(2, count($result['products']));
}

È abbastanza. Cerchiamo di aggiungere meno funzionalità al controller perché ciò rende difficili i test. Ma ovviamente c'è sempre del codice. Ad esempio, testiamo requisiti come: Mostra quei due record solo se hai effettuato l'accesso.

Quindi, il controller ha bisogno di una finta normalmente e di una piccola parte di dati hardcoded. Per un sistema di accesso forse un altro. Nel nostro test abbiamo un metodo helper per questo: setLoggedIn (). Ciò semplifica il test con login o senza login.

class ProductsController {
  public function index() {
    if($this->loggedIn()) {
      $this->set('products', $this->ProductModel->getAllActive());
    }
  }
}

Visualizzazioni

Il test delle viste è difficile. Innanzitutto separiamo la logica che si ripete. Lo inseriamo in Helpers e testiamo rigorosamente quelle classi. Ci aspettiamo sempre lo stesso risultato. Ad esempio, generateHtmlTableFromArray ().

Quindi abbiamo alcune viste specifiche del progetto. Non li testiamo. Non è davvero desiderabile testare l'unità. Li conserviamo per i test di integrazione. Poiché abbiamo preso gran parte del codice nelle viste, qui abbiamo un rischio inferiore.

Se inizi a testare quelli che probabilmente hai bisogno di cambiare i tuoi test ogni volta che cambi un pezzo di HTML che non è utile per la maggior parte dei progetti.

echo $this->tableHelper->generateHtmlTableFromArray($products);

Test d'integrazione

A seconda della tua piattaforma qui puoi lavorare con le storie degli utenti, ecc. Potrebbe essere webbased come Selenium o altre soluzioni comparabili.

Generalmente cariciamo semplicemente il database con i dispositivi e affermiamo quali dati dovrebbero essere disponibili. Per i test di integrazione completi utilizziamo generalmente requisiti molto globali. Quindi: impostare il prodotto su attivo e quindi verificare se il prodotto diventa disponibile.

Non testiamo di nuovo tutto, ad esempio se sono disponibili i campi giusti. Testiamo qui i requisiti più grandi. Dal momento che non vogliamo duplicare i nostri test dal controller o dalla vista. Se qualcosa è veramente una parte chiave / fondamentale della tua applicazione o per motivi di sicurezza (controlla la password NON è disponibile), li aggiungiamo per assicurarci che sia giusto.

I dati hard coded sono memorizzati nelle fixture.

function testIntegrationProductIndexLoggedIn() {
  $this->setLoggedIn();
  $result=$this->request('products/index');

  $expected='<table';
  $this->assertContains($expected, $result);

  // Some content from the fixture record
  $expected='<td>Product 1 name</td>';
  $this->assertContains($expected, $result);
}

Questa è un'ottima risposta a una domanda completamente diversa.
pdr,

Grazie per il feedback. Forse hai ragione, non l'ho menzionato in modo troppo specifico. Il motivo della risposta dettagliata è perché vedo una delle cose più difficili durante il test nella domanda posta. La panoramica di come i test in isolamento si adattano ai diversi tipi di test. Ecco perché ho aggiunto in ogni parte come vengono gestiti (o separati) i dati. Daremo un'occhiata per vedere se posso renderlo più chiaro.
Luc Franken,

La risposta è stata aggiornata con alcuni esempi di codice per spiegare come testare senza chiamare tutti i tipi di altre classi.
Luc Franken,

4

Se stai scrivendo test che coinvolgono molti DI e cablaggi, fino all'utilizzo di origini dati "reali", probabilmente hai abbandonato l'area di test unit unit e sei entrato nel dominio dei test di integrazione.

Per i test di integrazione, penso, non è una cattiva idea avere una logica di configurazione dei dati comune. L'obiettivo principale di tali test è dimostrare che tutto è configurato correttamente. Questo è piuttosto indipendente dai dati concreti inviati attraverso il tuo sistema.

Per i test unitari d'altra parte, consiglierei di mantenere l'obiettivo di una classe di test una singola classe "reale" e prendere in giro tutto il resto. Quindi dovresti davvero codificare i dati del test per assicurarti di aver coperto il maggior numero possibile di percorsi di bug speciali / precedenti.

Per aggiungere un elemento semi-hard-codificato / casuale ai test, mi piace introdurre fabbriche di modelli casuali. In un test che utilizza un'istanza del mio modello, utilizzo quindi queste fabbriche per creare un oggetto modello valido, ma completamente casuale e quindi codificare solo le proprietà che sono interessanti per il test a portata di mano. In questo modo si specificano tutti i dati rilevanti direttamente nel test, risparmiando nel contempo la necessità di specificare anche tutti i dati non pertinenti e (in una certa misura) verificare che non vi siano dipendenze indesiderate da altri campi del modello.


-1

Penso che sia abbastanza comune codificare la maggior parte dei dati per i tuoi test.

Considerare una situazione semplice in cui un determinato set di dati provoca un errore. È possibile creare un test unitario specifico per quei dati per esercitare la correzione e assicurarsi che il bug non ritorni. Nel tempo i tuoi test avranno una serie di dati che coprono una serie di casi di test.

I dati di test predefiniti consentono inoltre di creare un set di dati che copre una vasta e nota gamma di situazioni.

Detto questo, penso che valga anche la pena avere alcuni dati casuali nei tuoi test.


Hai davvero letto la domanda e non solo il titolo?
Jakob,

valore nell'avere alcuni dati casuali nei tuoi test - Sì, perché non c'è niente come provare a capire cosa è successo in un test una volta che fallisce ogni settimana.
pdr,

È utile disporre di dati casuali nei test per i test di pericolosità / sfocatura / input. Ma non nei tuoi test unitari, sarebbe un incubo.
glenatron,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.