I risultati previsti per i test unitari devono essere codificati?


30

I risultati attesi di un test unitario devono essere codificati in modo rigido o possono dipendere da variabili inizializzate? I risultati codificati o calcolati aumentano il rischio di introdurre errori nel test unitario? Ci sono altri fattori che non ho considerato?

Ad esempio, quale di questi due è un formato più affidabile?

[TestMethod]
public void GetPath_Hardcoded()
{
    MyClass target = new MyClass("fields", "that later", "determine", "a folder");
    string expected = "C:\\Output Folder\\fields\\that later\\determine\\a folder";
    string actual = target.GetPath();
    Assert.AreEqual(expected, actual,
        "GetPath should return a full directory path based on its fields.");
}

[TestMethod]
public void GetPath_Softcoded()
{
    MyClass target = new MyClass("fields", "that later", "determine", "a folder");
    string expected = "C:\\Output Folder\\" + string.Join("\\", target.Field1, target.Field2, target.Field3, target.Field4);
    string actual = target.GetPath();
    Assert.AreEqual(expected, actual,
        "GetPath should return a full directory path based on its fields.");
}

EDIT 1: in risposta alla risposta di DXM, l'opzione 3 è una soluzione preferita?

[TestMethod]
public void GetPath_Option3()
{
    string field1 = "fields";
    string field2 = "that later";
    string field3 = "determine";
    string field4 = "a folder";
    MyClass target = new MyClass(field1, field2, field3, field4);
    string expected = "C:\\Output Folder\\" + string.Join("\\", field1, field2, field3, field4);
    string actual = target.GetPath();
    Assert.AreEqual(expected, actual,
        "GetPath should return a full directory path based on its fields.");
}

2
Fai entrambi. Sul serio. I test possono e devono sovrapporsi. Esamina anche una sorta di test basati sui dati se ti trovi ad affrontare valori hardcoded.
Giobbe

Concordo sul fatto che la terza opzione è quella che mi piace usare. Non credo che l'opzione 1 danneggerebbe poiché si elimina la manipolazione durante la compilazione.
kwelch,

Entrambe le opzioni utilizzano la
codifica hardware

Risposte:


27

Penso che i risultati del valore atteso siano calcolati in casi di test più solidi e flessibili. Inoltre, usando buoni nomi di variabili nell'espressione che calcolano il risultato atteso, è molto più chiaro da dove provenga il risultato atteso.

Detto questo, nel tuo esempio specifico NON mi fiderei del metodo "Softcoded" perché usa il tuo SUT (sistema sotto test) come input per i tuoi calcoli. Se in MyClass è presente un bug in cui i campi non sono memorizzati correttamente, il test verrà effettivamente superato perché il calcolo del valore previsto utilizzerà una stringa errata proprio come target.GetPath ().

Il mio suggerimento sarebbe di calcolare il valore atteso laddove ha senso, ma assicurarsi che il calcolo non dipenda da alcun codice del SUT stesso.

In risposta all'aggiornamento dell'OP alla mia risposta:

Sì, in base alle mie conoscenze ma all'esperienza piuttosto limitata nel fare TDD, sceglierei l'opzione n. 3.


1
Buon punto! Non fare affidamento sull'oggetto non verificato nel test.
Hand-E-Food

non è la duplicazione del codice SUT?
Abyx,

1
in un certo senso, ma è così che si verifica che SUT funzioni. Se dovessimo usare lo stesso codice e venisse eliminato, non lo sapresti mai. Naturalmente, se per eseguire il calcolo, è necessario duplicare molto SUT, allora forse l'opzione n. 1 potrebbe migliorare, codificare semplicemente il valore.
DXM,

16

E se il codice fosse il seguente:

MyTarget() // constructor
{
   Field1 = Field2 = Field3 = Field4 = "";
}

Il tuo secondo esempio non catturerebbe il bug, ma il primo esempio lo farebbe.

In generale, raccomanderei di evitare i soft-coding perché potrebbero nascondere bug. Per esempio:

string expected = "C:\\Output Folder" + string.Join("\\", target.Field1, target.Field2, target.Field3, target.Field4);

Riesci a individuare il problema? Non commetteresti lo stesso errore in una versione codificata. È più difficile ottenere i calcoli corretti rispetto ai valori codificati. Ecco perché preferisco lavorare con valori hardcoded rispetto a quelli soft-coded.

Ma ci sono eccezioni. Cosa succede se il codice deve essere eseguito su Windows e Linux? Non solo il percorso dovrà essere diverso, ma dovrà utilizzare diversi separatori di percorso! Calcolare il percorso usando funzioni che astraggono la differenza tra potrebbe avere senso in quel contesto.


Ho sentito quello che stai dicendo e questo mi dà qualcosa da considerare. Il softcoding si basa sul passaggio di altri miei casi di test (come ConstructorShouldCorrectlyInitialiseFields). L'errore che descrivi verrebbe referenziato da altri test unitari falliti.
Hand-E-Food

@ Hand-E-Food, sembra che tu stia scrivendo test su singoli metodi dei tuoi oggetti. Non farlo. Dovresti scrivere test che controllano la correttezza dell'intero oggetto insieme non i singoli metodi. Altrimenti i tuoi test saranno fragili rispetto alle modifiche all'interno dell'oggetto.
Winston Ewert,

Non sono sicuro di seguirlo. L'esempio che ho dato è stato puramente ipotetico, uno scenario di facile comprensione. Sto scrivendo unit test per testare membri pubblici di classi e oggetti. È quello il modo corretto di usarli?
Hand-E-Food

@ Hand-E-Food, se ti capisco correttamente, il tuo test ConstructShouldCorrectlyInitialiseFields invocherebbe il costruttore e quindi affermerebbe che i campi sono impostati correttamente. Ma non dovresti farlo. Non dovresti preoccuparti di cosa stanno facendo i campi interni. Dovresti solo affermare che il comportamento esterno dell'oggetto è corretto. Altrimenti, potrebbe venire il giorno in cui è necessario sostituire l'implementazione interna. Se hai fatto affermazioni sullo stato interno, tutti i test si interromperanno. Ma se hai fatto solo affermazioni sul comportamento esterno, tutto funzionerà comunque.
Winston Ewert,

@ Winston - In realtà sto lavorando al libro xUnit Test Patterns e prima ancora che The Art of Unit Testing. Non farò finta di sapere di cosa sto parlando, ma mi piacerebbe pensare di aver preso qualcosa da quei libri. Entrambi i libri raccomandano caldamente che ogni metodo di test dovrebbe testare il minimo assoluto e che si dovrebbero avere molti casi di test per testare l'intero oggetto. In questo modo quando le interfacce o le funzionalità cambiano, dovresti aspettarti di risolvere solo pochi metodi di prova, piuttosto che la maggior parte di essi. E poiché sono piccoli, i cambiamenti dovrebbero essere più facili.
DXM il

4

Secondo me, entrambi i tuoi suggerimenti sono tutt'altro che ideali. Il modo ideale per farlo è questo:

[TestMethod]
public void GetPath_Hardcoded()
{
    const string f1 = "fields"; const string f2 = "that later"; 
    const string f3 = "determine"; const string f4 = "a folder";

    MyClass target = new MyClass( f1, f2, f3, f4 );
    string expected = "C:\\Output Folder\\" + string.Join("\\", f1, f2, f3, f4);
    string actual = target.GetPath();
    Assert.AreEqual(expected, actual,
        "GetPath should return a full directory path based on its fields.");
}

In altre parole, il test dovrebbe funzionare esclusivamente in base all'input e all'output dell'oggetto e non in base allo stato interno dell'oggetto. L'oggetto dovrebbe essere trattato come una scatola nera. (Ignoro altri problemi, come l'inadeguatezza dell'uso di string.Join invece di Path.Combine, perché questo è solo un esempio.)


1
Non tutti i metodi sono funzionali: molti hanno correttamente effetti collaterali che cambiano lo stato di alcuni oggetti. Un test unitario per un metodo con effetti collaterali avrebbe probabilmente bisogno di valutare lo stato degli oggetti interessati dal metodo.
Matthew Flynn,

Quindi quello stato verrebbe considerato come l'output del metodo. Lo scopo di questo test di esempio è verificare il metodo GetPath (), non il costruttore di MyClass. Leggi la risposta di @ DXM, fornisce un'ottima ragione per adottare l'approccio black box.
Mike Nakis,

@MatthewFlynn, quindi dovresti testare i metodi interessati da quello stato. Lo stato interno esatto è un dettaglio di implementazione e nessuna attività del test.
Winston Ewert,

@MatthewFlynn, solo per chiarire, è correlato all'esempio mostrato o qualcos'altro da considerare per altri test unitari? Ho potuto vedere che importa qualcosa del tipo target.Dispose(); Assert.IsTrue(target.IsDisposed);(un esempio molto semplice.)
Hand-E-Food

Anche in questo caso, la proprietà IsDisposed è (o dovrebbe essere) una parte indispensabile dell'interfaccia pubblica della classe e non un dettaglio di implementazione. (L'interfaccia IDispose non fornisce tale proprietà, ma questo è un peccato.)
Mike Nakis

2

Ci sono due aspetti nella discussione:

1. Utilizzo del target stesso per il test case
La prima domanda è: è possibile / è possibile utilizzare la classe stessa per fare affidamento e ottenere parte del lavoro svolto nello stub di test? - La risposta è NO poiché, in generale, non dovresti mai fare ipotesi sul codice che stai testando. Se ciò non viene eseguito correttamente, nel tempo i bug diventano immuni ad alcuni test unitari.

2. L'hardcoding
dovrebbe essere hard code ? Anche in questo caso la risposta è No . perché come qualsiasi software - la codifica rigida delle informazioni diventa difficile quando le cose si evolvono. Ad esempio, quando si desidera modificare nuovamente il percorso sopra riportato, è necessario scrivere un'unità aggiuntiva o continuare a modificare. Un metodo migliore consiste nel mantenere la data di input e valutazione derivata dalla configurazione separata che può essere facilmente adattata.

ad esempio, ecco come farei bene al troncone di prova.

[TestMethod]
public void GetPath_Tested(int CaseId)
{
    testParams = GetTestConfig(caseID,"testConfig.txt"); // some wrapper that does read line and chops the field. 
    MyClass target = new MyClass(testParams.field1, testParams.field2);
    string expected = testParams.field5;
    string actual = target.GetPath();
    Assert.AreEqual(expected, actual,
        "GetPath should return a full directory path based on its fields.");
}

0

Ci sono molti concetti possibili, fatti alcuni esempi per vedere la differenza

[TestMethod]
public void GetPath_Softcoded()
{
    //Hardcoded since you want to see what you expect is most simple and clear
    string expected = "C:\\Output Folder\\fields\\that later\\determine\\a folder";

    //If this test should also use a mocked filesystem it might be that you want to use
    //some base directory, which you could set in the setUp of your test class
    //that is usefull if you you need to run the same test on different environments
    string expected = this.outputPath + "fields\\that later\\determine\\a folder";


    //another readable way could be interesting if you have difficult variables needed to test
    string fields = "fields";
    string thatLater = "that later";
    string determine = "determine";
    string aFolder = "a folder";
    string expected = this.outputPath + fields + "\\" + thatLater + "\\" + determine + "\\" + aFolder;
    MyClass target = new MyClass(fields, thatLater, determine, aFolder);

    //in general testing with real words is not needed, so code could be shorter on that
    //for testing difficult folder names you write a separate test anyway
    string f1 = "f1";
    string f2 = "f2";
    string f3 = "f3";
    string f4 = "f4";
    string expected = this.outputPath + f1 + "\\" + f2 + "\\" + f3 + "\\" + f4;
    MyClass target = new MyClass(f1, f2, f3, f4);

    //so here we start to see a structure, it looks more like an array of fields
    //so what would make testing more interesting with lots of variables is the use of a data provider
    //the data provider will re-use your test with many different kinds of inputs. That will reduce the amount of duplication of code for testing
    //http://msdn.microsoft.com/en-us/library/ms182527.aspx


    The part where you compare already seems correct
    MyClass target = new MyClass(fields, thatLater, determine, aFolder);

    string actual = target.GetPath();
    Assert.AreEqual(expected, actual,
        "GetPath should return a full directory path based on its fields.");
}

Riassumendo: in generale il tuo primo test appena codificato ha più senso per me perché è semplice, diretto al punto ecc. Se inizi a codificare un percorso troppe volte inseriscilo nel metodo di configurazione.

Per ulteriori test strutturati futuri andrei a controllare le origini dati in modo da poter aggiungere più righe di dati se hai bisogno di più situazioni di test.


0

I moderni framework di test ti consentono di fornire parametri al tuo metodo. Farei leva su quelli:

[TestCase("fields", "that later", "determine", "a folder", @"C:\Output Folder\fields\that later\determine\a folder")]
public void GetPathShouldReturnFullDirectoryPathBasedOnItsFields(
    string field1, string field2, string field3, string field,
    string expected)
{
    MyClass target = new MyClass(field1, field2, field3, field4);
    string actual = target.GetPath();
    Assert.AreEqual(expected, actual,
        "GetPath should return a full directory path based on its fields.");
}

A mio avviso, ci sono diversi vantaggi:

  1. Gli sviluppatori sono spesso tentati di copiare le parti apparentemente semplici del codice dal loro SUT nei loro test unitari. Come sottolinea Winston , quelli possono ancora avere bug insidiosi nascosti in loro. "Codifica hard" il risultato atteso aiuta a evitare situazioni in cui il codice di test non è corretto per lo stesso motivo per cui il codice originale non è corretto. Ma se un cambiamento nei requisiti ti costringe a rintracciare stringhe codificate all'interno di dozzine di metodi di test, questo può essere fastidioso. Avere tutti i valori codificati in un unico posto, al di fuori della logica del test, ti offre il meglio di entrambi i mondi.
  2. È possibile aggiungere test per diversi input e output previsti con una singola riga di codice. Questo ti incoraggia a scrivere più test, mantenendo il codice di test DRY e facile da mantenere. Trovo che, poiché è così economico aggiungere test, la mia mente è aperta a nuovi casi di test a cui non avrei pensato se avessi dovuto scrivere un metodo completamente nuovo per loro. Ad esempio, quale comportamento mi aspetterei se uno degli input avesse un punto? Una barra rovesciata? E se uno fosse vuoto? O spazi bianchi? O iniziato o finito con spazi bianchi?
  3. Il framework di test tratterà ogni TestCase come il proprio test, anche inserendo gli input e gli output forniti nel nome del test. Se tutti i TestCase superano uno solo, è super facile vedere quale si è rotto e come era diverso da tutti gli altri.
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.