Ho davvero bisogno di un framework di unit test?


19

Attualmente nel mio lavoro, disponiamo di un'ampia suite di test unitari per la nostra applicazione C ++. Tuttavia non utilizziamo un framework di unit test. Usano semplicemente una macro C che sostanzialmente avvolge un'asserzione e una cout. Qualcosa di simile a:

VERIFY(cond) if (!(cond)) {std::cout << "unit test failed at " << __FILE__ << "," << __LINE__; asserst(false)}

Quindi creiamo semplicemente funzioni per ciascuno dei nostri test come

void CheckBehaviorYWhenXHappens()
{
    // a bunch of code to run the test
    //
    VERIFY(blah != blah2);
    // more VERIFY's as needed
}

Il nostro server CI rileva "Unit Test Failed" e fallisce la compilazione, inviando il messaggio via e-mail agli sviluppatori.

E se abbiamo un codice di installazione duplicato, lo refattiamo semplicemente come faremmo con qualsiasi altro codice duplicato che avremmo in produzione. Lo avvolgiamo dietro le funzioni di supporto, facciamo in modo che alcune classi di test concludano la creazione di scenari di uso frequente.

So che ci sono framework come CppUnit e boost unit test. Mi chiedo che valore aggiungono questi? Mi sto perdendo ciò che portano sul tavolo? C'è qualcosa di utile che potrei guadagnare da loro? Sono titubante nell'aggiungere una dipendenza, a meno che non aggiunga un valore reale, soprattutto perché sembra che ciò che abbiamo sia completamente semplice e funzioni bene.

Risposte:


8

Come altri hanno già detto, hai già il tuo framework semplice e fatto in casa.

Sembra banale farne uno. Tuttavia, ci sono alcune altre caratteristiche di un framework di unit test che non sono così facili da implementare, perché richiedono una conoscenza avanzata della lingua. Le caratteristiche che di solito richiedo da un framework di test e che non sono così facili da homebrew sono:

  • Raccolta automatica di casi di test. Vale a dire che definire un nuovo metodo di prova dovrebbe essere sufficiente per farlo eseguire. JUnit raccoglie automaticamente tutti i metodi i cui nomi iniziano con test, NUnit ha l' [Test]annotazione, Boost.Test utilizza le macro BOOST_AUTO_TEST_CASEe BOOST_FIXTURE_TEST_CASE.

    È principalmente convenienza, ma ogni piccola comodità che puoi ottenere migliora le possibilità che gli sviluppatori scriveranno effettivamente i test che dovrebbero e che li collegheranno correttamente. Se hai lunghe istruzioni, qualcuno ne mancherà una parte ora e forse e forse alcuni test non saranno in esecuzione e nessuno se ne accorgerà.

  • Possibilità di eseguire casi di test selezionati, senza modificare il codice e ricompilare. Qualsiasi framework di test unitario decente consente di specificare quali test si desidera eseguire sulla riga di comando. Se vuoi eseguire il debug su unit test (è il punto più importante in essi per molti sviluppatori), devi essere in grado di selezionarne solo alcuni da eseguire, senza modificare il codice ovunque.

    Supponiamo che tu abbia appena ricevuto la segnalazione di bug n. 4211 e che possa essere riprodotta con unit test. Quindi ne scrivi uno, ma devi dire al corridore di eseguire proprio quel test, in modo da poter eseguire il debug di ciò che è effettivamente sbagliato lì.

  • Capacità di contrassegnare i test previsti fallimenti, per caso di test, senza modificare i controlli stessi. In realtà abbiamo cambiato framework al lavoro per ottenerlo.

    Qualsiasi suite di test di dimensioni decenti avrà dei test, che falliscono perché le funzionalità che testano non sono ancora state implementate, non sono ancora finite, nessuno ha avuto il tempo di ripararle ancora o qualcosa del genere. Senza la possibilità di contrassegnare i test come fallimenti previsti, non si noterà un altro errore quando ce ne sono regolarmente, quindi i test smettono di servire al loro scopo principale.


grazie penso che questa sia la risposta migliore. In questo momento la mia macro fa il suo lavoro, ma non posso fare nessuna delle funzionalità che menzioni.
Doug T.

1
@Jan Hudec "È principalmente convenienza, ma ogni piccola comodità che puoi ottenere migliora le possibilità che gli sviluppatori scriveranno effettivamente i test che dovrebbero e che li collegheranno correttamente."; Tutti i framework di test sono (1) non banali da installare, spesso hanno istruzioni di installazione più obsolete o non esaustive rispetto alle istruzioni aggiornate aggiornate; (2) se ti impegni direttamente in un framework di test, senza un'interfaccia nel mezzo, sei sposato con esso, cambiare framework non è sempre facile.
Dmitry

@Jan Hudec Se prevediamo di avere più persone a scrivere unit test, dovremo avere più risultati su google per "Che cos'è un unit test" rispetto a "Che cos'è unit test". Non ha senso eseguire Unit Test se non si ha idea di cosa sia un Unit Test indipendente da qualsiasi Frame Test Unit o dalla definizione di Unit Test. Non è possibile effettuare Test unitari a meno che non si abbia una forte comprensione di cosa sia un Test unitario, poiché altrimenti non ha senso eseguire Test unitari.
Dmitry

Non compro questo argomento di convenienza. Scrivere un codice di prova è molto difficile se lasci il banale mondo degli esempi. Tutti questi mockup, configurazioni, librerie, programmi server mockup esterni ecc. Tutti richiedono che tu conosca il framework di test da dentro e fuori.
Lothar,

@Lothar, sì, è tutto molto lavoro e molto da imparare, ma dover ancora scrivere più volte una semplice caldaia perché mancano un paio di utilità utili rende il lavoro molto meno piacevole e che fa una notevole differenza in termini di efficacia.
Jan Hudec,

27

Sembra che tu usi già un framework, fatto in casa.

Qual è il valore aggiunto di framework più popolari? Direi che il valore che aggiungono è che quando devi scambiare codice con persone esterne alla tua azienda, puoi farlo, poiché si basa sul framework che è noto e ampiamente utilizzato .

Un framework fatto in casa, d'altra parte, ti obbliga a non condividere mai il tuo codice o a fornire il framework stesso, che può diventare ingombrante con la crescita del framework stesso.

Se dai il tuo codice a un collega così com'è, senza spiegazioni e senza framework di unit test, non sarebbe in grado di compilarlo.

Un secondo inconveniente dei framework fatti in casa è la compatibilità . I più diffusi framework di unit test tendono a garantire la compatibilità con diversi IDE, sistemi di controllo versione, ecc. Per il momento, potrebbe non essere molto importante per te, ma cosa accadrà se un giorno dovrai cambiare qualcosa nel tuo server CI o migrare a un nuovo IDE o un nuovo VCS? Reinventerai la ruota?

Ultimo ma non meno importante, i framework più grandi offrono più funzionalità che potresti dover implementare nel tuo framework un giorno. Assert.AreEqual(expected, actual)non è sempre abbastanza. E se fosse necessario:

  • misurare la precisione?

    Assert.AreEqual(3.1415926535897932384626433832795, actual, 25)
    
  • test nullo se funziona per troppo tempo? La reimplementazione di un timeout potrebbe non essere semplice anche nelle lingue che facilitano la programmazione asincrona.

  • testare un metodo che prevede di generare un'eccezione?

  • hai un codice più elegante?

    Assert.Verify(a == null);
    

    va bene, ma non è più espressivo il tuo intento di scrivere la riga successiva?

    Assert.IsNull(a);
    

Il "framework" che utilizziamo è tutto in un file header molto piccolo e segue la semantica di assert. Quindi non sono troppo preoccupato per gli svantaggi che elenchi.
Doug T.,

4
Considero le asserzioni la parte più banale del framework di test. Il corridore che raccoglie ed esegue i casi di test e controlla i risultati è la parte non banale importante.
Jan Hudec,

@Jan non la seguo del tutto. Il mio corridore è una routine principale comune a tutti i programmi C ++. Un corridore del framework di unit test fa qualcosa di più sofisticato e utile?
Doug T.,

1
Il tuo framework consente solo la semantica di assert ed esecuzione di test in un metodo principale ... finora. Aspetta solo di dover raggruppare le tue asserzioni in più scenari, raggruppare scenari correlati in base a dati inizializzati, ecc.
James Kingsbery,

@DougT .: Sì, un discreto runner del framework di unit test fa cose utili più sofisticate. Vedi la mia risposta completa.
Jan Hudec,

4

Come altri hanno già detto, hai già il tuo framework fatto in casa.

L'unica ragione che posso vedere per l'utilizzo di qualche altro framework di test sarebbe dal punto di vista della "conoscenza comune" del settore. I nuovi sviluppatori non dovrebbero imparare a casa tua (anche se sembra molto semplice).

Inoltre, altri framework di test potrebbero avere più funzionalità di cui potresti trarre vantaggio.


1
Concordato. Se non stai incontrando limitazioni con la tua attuale strategia di test, vedo poche ragioni per cambiare. Un buon framework fornirebbe probabilmente migliori capacità di organizzazione e reporting, ma dovresti giustificare il lavoro aggiuntivo richiesto per l'integrazione con la tua base di codice (incluso il tuo sistema di compilazione).
TMN,

3

Hai già un framework anche se è semplice.

I principali vantaggi di un framework più ampio, come li vedo, sono la capacità di avere molti tipi diversi di asserzioni (come l'asserzione di aumenti), un ordine logico ai test unitari e la possibilità di eseguire solo un sottoinsieme di unit test in un tempo. Inoltre, è possibile seguire il modello dei test xUnit se è possibile seguirlo, ad esempio setUP () e tearDown (). Naturalmente, questo ti blocca in detto framework. Si noti che alcuni framework hanno una migliore integrazione fittizia rispetto ad altri, ad esempio google mock e test.

Quanto tempo impiegherai per refactificare tutti i test unitari in un nuovo framework? Giorni o poche settimane forse ne valgono la pena, ma forse di più non così tanto.


2

Per come la vedo io, entrambi avete il vantaggio e siete in "svantaggio" (sic).

Il vantaggio è che hai un sistema con cui ti senti a tuo agio e che funziona per te. Sei contento che confermi la validità del tuo prodotto e probabilmente non troverai alcun valore commerciale nel tentativo di modificare tutti i test per qualcosa che utilizza un framework diverso. Se riesci a riformattare il tuo codice e i tuoi test raccolgono le modifiche - o meglio ancora, se puoi modificare i tuoi test e il tuo codice esistente fallisce i test fino a quando non viene refactored, allora hai coperto tutte le tue basi. Tuttavia...

Uno dei vantaggi di avere un'API di unit test ben progettata è che c'è molto supporto nativo nella maggior parte degli IDE moderni. Ciò non influirà sul VI hard-core ed emacs degli utenti là fuori che ghignano agli utenti di Visual Studio là fuori, ma per quelli che usano un buon IDE, hai la possibilità di eseguire il debug dei test ed eseguirli all'interno l'IDE stesso. Questo è buono, tuttavia c'è un vantaggio ancora maggiore a seconda del framework che usi e che è nella lingua usata per testare il tuo codice.

Quando dico linguaggio , non sto parlando di un linguaggio di programmazione, ma piuttosto di un ricco insieme di parole racchiuse in una sintassi fluente che fa leggere il codice di prova come una storia. In particolare, sono diventato un sostenitore dell'uso dei framework BDD . La mia API DotD BDD preferita personale è StoryQ, ma ce ne sono molti altri con lo stesso scopo di base, ovvero estrarre un concetto da un documento dei requisiti e scriverlo in codice in modo simile a come è scritto nelle specifiche. Le API davvero buone vanno comunque oltre, intercettando ogni singola istruzione all'interno di un test e indicando se tale istruzione è stata eseguita correttamente o non è riuscita. Questo è incredibilmente utile, poiché puoi vedere l'intero test eseguito senza tornare presto, il che significa che i tuoi sforzi di debug diventano incredibilmente efficienti in quanto devi solo focalizzare la tua attenzione sulle parti del test fallite, senza bisogno di decodificare l'intera chiamata sequenza. L'altra cosa interessante è che l'output del test mostra tutte queste informazioni,

Come esempio di ciò di cui sto parlando, confronta quanto segue:

Utilizzando gli asserti:

Assert(variable_A == expected_value_1); // if this fails...
Assert(variable_B == expected_value_2); // ...this will not execute
Assert(variable_C == expected_value_3); // ...and nor will this!

Utilizzando un'API BDD fluente: (Immagina che i bit in corsivo siano fondamentalmente puntatori a metodi)

WithScenario("Test Scenario")
    .Given(*AConfiguration*) // each method
    .When(*MyMethodToTestIsCalledWith*, variable_A, variable_B, variable_C) // in the
    .Then(*ExpectVariableAEquals*, expected_value_1) // Scenario will
        .And(*ExpectVariableBEquals*, expected_value_2) // indicate if it has
        .And(*ExpectVariableCEquals*, expected_value_3) // passed or failed execution.
    .Execute();

Ora concesso che la sintassi BDD sia più lunga e più complessa, e questi esempi sono terribilmente inventati, tuttavia per situazioni di test molto complesse in cui molte cose stanno cambiando in un sistema a causa di un determinato comportamento del sistema, la sintassi BDD ti offre un chiaro descrizione di ciò che si sta testando e di come è stata definita la configurazione del test e si può mostrare questo codice a un non programmatore e capiranno immediatamente cosa sta succedendo. Inoltre, se "variabile_A" non supera il test in entrambi i casi, l'esempio Asserti non verrebbe eseguito oltre la prima asserzione fino a quando non si fosse risolto il problema, mentre l'API BDD avrebbe eseguito a sua volta tutti i metodi chiamati nella catena, indicando quale le singole parti della dichiarazione erano in errore.

Personalmente trovo che questo approccio funzioni molto meglio dei più tradizionali framework xUnit, nel senso che la lingua dei test è la stessa lingua che i tuoi clienti parleranno dei loro requisiti logici. Anche così, sono riuscito a utilizzare i framework xUnit in uno stile simile senza dover inventare un'API di test completa per supportare i miei sforzi e, sebbene le affermazioni continueranno effettivamente a cortocircuitare da sole, leggono in modo più chiaro. Per esempio:

Utilizzando Nunit :

[Test]
void TestMyMethod()
{
    const int theExpectedValue = someValue;

    GivenASetupToTestMyMethod();

    var theActualValue = WhenIExecuteMyMethodToTest();

    Assert.That(theActualValue, Is.EqualTo(theExpectedValue)); // nice, but it's not BDD
}

Se decidi di esplorare utilizzando un'API di unit test, il mio consiglio è di sperimentare un numero elevato di API diverse per un po 'di tempo e di tenere e aprire la mente sul tuo approccio. Mentre sostengo personalmente BDD, le tue esigenze aziendali potrebbero richiedere qualcosa di diverso per le circostanze del tuo team. La chiave tuttavia è evitare di indovinare il sistema esistente. Puoi sempre supportare i tuoi test esistenti con alcuni test usando un'altra API, se necessario, ma sicuramente non consiglierei un enorme test di riscrittura solo per rendere tutto uguale. Poiché il codice legacy non è più in uso, è possibile sostituirlo facilmente e i relativi test con un nuovo codice e i test utilizzando un'API alternativa, senza che sia necessario investire in uno sforzo maggiore che non fornirà necessariamente alcun valore reale per l'azienda. Per quanto riguarda l'utilizzo di un'API di unit test,


1

Quello che hai è semplice e porta a termine il lavoro. Se funziona per te, fantastico. Non hai bisogno di un framework di test unitario mainstream, e esiterei ad andare al lavoro di porting di una libreria esistente di unit test su un nuovo framework. Penso che il maggior valore dei framework di unit test sia quello di ridurre la barriera all'ingresso; hai appena iniziato a scrivere test, perché il framework è già in atto. Hai superato quel punto, quindi non otterrai questo vantaggio.

L'altro vantaggio dell'utilizzo di un framework mainstream - ed è un vantaggio minore, IMO - è che i nuovi sviluppatori potrebbero già essere al passo con qualunque framework tu stia usando, e quindi richiederà meno formazione. In pratica, con un approccio semplice come quello che hai descritto, questo non dovrebbe essere un grosso problema.

Inoltre, la maggior parte dei framework mainstream ha alcune caratteristiche che il tuo framework potrebbe o meno avere. Queste funzionalità riducono il codice idraulico e rendono più semplice e veloce la scrittura di casi di test:

  • Esecuzione automatica di casi di test, utilizzando convenzioni di denominazione, annotazioni / attributi, ecc.
  • Varie affermazioni più specifiche, in modo da non dover scrivere una logica condizionale per tutte le asserzioni o catturare eccezioni per affermare il loro tipo.
  • Classificazione dei casi di test, in modo da poter eseguire facilmente sottoinsiemi di essi.
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.