Domanda

Sto scrivendo un componente che, dato un file ZIP, deve:

  1. Decomprimi il file.
  2. Trova una DLL specifica tra i file decompressi.
  3. Carica quella dll tramite reflection e invoca un metodo su di essa.

Mi piacerebbe provare l'unità di questo componente.

Sono tentato di scrivere codice che si occupa direttamente del file system:

void DoIt()
{
   Zip.Unzip(theZipFile, "C:\\foo\\Unzipped");
   System.IO.File myDll = File.Open("C:\\foo\\Unzipped\\SuperSecret.bar");
   myDll.InvokeSomeSpecialMethod();
}

Ma la gente spesso dice: "Non scrivere test unitari che si basano su file system, database, rete, ecc."

Se dovessi scrivere questo in un modo amichevole unit test, suppongo che sarebbe simile a questo:

void DoIt(IZipper zipper, IFileSystem fileSystem, IDllRunner runner)
{
   string path = zipper.Unzip(theZipFile);
   IFakeFile file = fileSystem.Open(path);
   runner.Run(file);
}

Yay! Ora è testabile; Sono in grado di alimentare doppi di prova (simulazioni) con il metodo DoIt. Ma a quale costo? Ora ho dovuto definire 3 nuove interfacce solo per renderlo testabile. E cosa sto testando esattamente? Sto testando che la mia funzione DoIt interagisce correttamente con le sue dipendenze. Non verifica che il file zip sia stato decompresso correttamente, ecc.

Non mi sento più come se stessi testando la funzionalità. Mi sento come se stessi solo testando le interazioni di classe.

La mia domanda è questa : qual è il modo corretto di testare l'unità che dipende dal file system?

modifica Sto usando .NET, ma il concetto potrebbe applicare anche Java o codice nativo.

È stato utile?

Soluzione

Non c'è davvero nulla di sbagliato in questo, è solo una questione se lo chiami un test unitario o un test di integrazione. Devi solo assicurarti che se interagisci con il file system, non ci sono effetti collaterali indesiderati. In particolare, assicurati di ripulire dopo te stesso - elimina tutti i file temporanei che hai creato - e di non sovrascrivere accidentalmente un file esistente che ha lo stesso nome di un file temporaneo che stavi utilizzando. Usa sempre percorsi relativi e non percorsi assoluti.

Sarebbe anche una buona idea chdir () in una directory temporanea prima di eseguire il test e chdir () in seguito.

Altri suggerimenti

  

Yay! Ora è testabile; Sono in grado di alimentare doppi di prova (simulazioni) con il metodo DoIt. Ma a quale costo? Ora ho dovuto definire 3 nuove interfacce solo per renderlo testabile. E cosa sto testando esattamente? Sto testando che la mia funzione DoIt interagisce correttamente con le sue dipendenze. Non verifica che il file zip sia stato decompresso correttamente, ecc.

Hai colpito l'unghia proprio sulla sua testa. Quello che vuoi testare è la logica del tuo metodo, non necessariamente se un vero file può essere indirizzato. Non è necessario verificare (in questo test unitario) se un file è stato decompresso correttamente, il metodo lo dà per scontato. Le interfacce sono preziose da sole perché forniscono astrazioni su cui è possibile programmare, piuttosto che fare affidamento implicitamente o esplicitamente su un'implementazione concreta.

La tua domanda espone una delle parti più difficili dei test per gli sviluppatori che ci stanno appena entrando:

" Che diavolo devo testare? "

Il tuo esempio non è molto interessante perché incolla solo alcune chiamate API insieme, quindi se dovessi scrivere un test unit per esso finiresti per affermare che i metodi sono stati chiamati. Test come questo associano strettamente i dettagli dell'implementazione al test. Questo è negativo perché ora devi cambiare il test ogni volta che modifichi i dettagli di implementazione del tuo metodo perché la modifica dei dettagli di implementazione interrompe i tuoi test!

Effettuare test non corretti è in realtà peggio che non averne alcun test.

Nel tuo esempio:

void DoIt(IZipper zipper, IFileSystem fileSystem, IDllRunner runner)
{
   string path = zipper.Unzip(theZipFile);
   IFakeFile file = fileSystem.Open(path);
   runner.Run(file);
}

Sebbene tu possa passare in giro, non c'è logica nel metodo da testare. Se dovessi tentare un test unitario per questo potrebbe essere simile a questo:

// Assuming that zipper, fileSystem, and runner are mocks
void testDoIt()
{
  // mock behavior of the mock objects
  when(zipper.Unzip(any(File.class)).thenReturn("some path");
  when(fileSystem.Open("some path")).thenReturn(mock(IFakeFile.class));

  // run the test
  someObject.DoIt(zipper, fileSystem, runner);

  // verify things were called
  verify(zipper).Unzip(any(File.class));
  verify(fileSystem).Open("some path"));
  verify(runner).Run(file);
}

Complimenti, in pratica hai copiato e incollato i dettagli di implementazione del tuo metodo DoIt () in un test. Buona manutenzione.

Quando scrivi i test vuoi testare il COSA e non il COME . Vedi Test della scatola nera per ulteriori informazioni

Il COSA è il nome del tuo metodo (o almeno dovrebbe essere). Il COME sono tutti i piccoli dettagli di implementazione presenti nel tuo metodo. Buoni test ti consentono di scambiare il COME senza interrompere il COSA .

Pensaci in questo modo, chiediti:

" Se modifico i dettagli di implementazione di questo metodo (senza alterare il contratto pubblico), interromperò i miei test? "

Se la risposta è sì, stai testando il COME e non il COSA .

Per rispondere alla tua domanda specifica sul test del codice con dipendenze del file system, diciamo che hai avuto qualcosa di un po 'più interessante in corso su un file e che volevi salvare il contenuto codificato Base64 di un byte [] in un file. Puoi utilizzare gli stream per questo per verificare che il tuo codice faccia la cosa giusta senza dover controllare come lo fa. Un esempio potrebbe essere qualcosa del genere (in Java):

interface StreamFactory {
    OutputStream outStream();
    InputStream inStream();
}

class Base64FileWriter {
    public void write(byte[] contents, StreamFactory streamFactory) {
        OutputStream outputStream = streamFactory.outStream();
        outputStream.write(Base64.encodeBase64(contents));
    }
}

@Test
public void save_shouldBase64EncodeContents() {
    OutputStream outputStream = new ByteArrayOutputStream();
    StreamFactory streamFactory = mock(StreamFactory.class);
    when(streamFactory.outStream()).thenReturn(outputStream);

    // Run the method under test
    Base64FileWriter fileWriter = new Base64FileWriter();
    fileWriter.write("Man".getBytes(), streamFactory);

    // Assert we saved the base64 encoded contents
    assertThat(outputStream.toString()).isEqualTo("TWFu");
}

Il test utilizza un ByteArrayOutputStream ma nell'applicazione (usando l'iniezione delle dipendenze) il vero StreamFactory (forse chiamato FileStreamFactory) restituisce FileOutputStream da outputStream () e scriverebbe in un File .

La cosa interessante del metodo write qui è che stava scrivendo i contenuti codificati Base64, quindi è per questo che abbiamo testato. Per il tuo metodo DoIt () , questo sarebbe testato in modo più appropriato con un test di integrazione .

Sono reticente a inquinare il mio codice con tipi e concetti che esistono solo per facilitare i test unitari. Certo, se rende il design più pulito e migliore, allora ottimo, ma penso che spesso non sia così.

La mia opinione su questo è che i test unitari farebbero tutto il possibile, il che potrebbe non essere una copertura del 100%. In effetti, potrebbe essere solo del 10%. Il punto è che i test unitari dovrebbero essere veloci e non avere dipendenze esterne. Potrebbero testare casi come " questo metodo genera un ArgumentNullException quando si passa a null per questo parametro " ;.

Vorrei quindi aggiungere test di integrazione (anche automatizzati e probabilmente utilizzando lo stesso framework di unit test) che possono avere dipendenze esterne e testare scenari end-to-end come questi.

Quando misuro la copertura del codice, misuro sia i test unitari che quelli di integrazione.

Non c'è niente di sbagliato nel colpire il file system, basta considerarlo un test di integrazione piuttosto che un test unitario. Scamberei il percorso hard coded con un percorso relativo e creerei una sottocartella TestData per contenere le zip per i test unitari.

Se i test di integrazione impiegano troppo tempo per essere eseguiti, separali in modo che non vengano eseguiti con la stessa frequenza dei test rapidi dell'unità.

Sono d'accordo, a volte penso che i test basati sull'interazione possano causare troppi accoppiamenti e spesso finiscano per non fornire abbastanza valore. Vuoi davvero provare a decomprimere il file qui non solo per verificare che stai chiamando i metodi giusti.

Un modo sarebbe scrivere il metodo di decompressione per prendere InputStreams. Quindi il test unitario potrebbe costruire un tale InputStream da un array di byte usando ByteArrayInputStream. Il contenuto di quell'array di byte potrebbe essere una costante nel codice unit test.

Questo sembra essere più un test di integrazione in quanto dipende da un dettaglio specifico (il file system) che potrebbe cambiare, in teoria.

Vorrei astrarre il codice che si occupa del sistema operativo nel suo modulo (classe, assembly, jar, qualunque cosa). Nel tuo caso vuoi caricare una DLL specifica se trovata, quindi crea un'interfaccia IDllLoader e la classe DllLoader. La tua app ha acquisito la DLL da DllLoader usando l'interfaccia e testalo ... dopotutto non sei responsabile del codice di decompressione giusto?

Supponendo che "interazioni del file system" sono ben testati nel framework stesso, creano il tuo metodo per lavorare con i flussi e testalo. L'apertura di un FileStream e il passaggio al metodo possono essere esclusi dai test, poiché FileStream.Open è ben testato dai creatori del framework.

Non dovresti testare l'interazione di classe e la chiamata di funzione. invece dovresti considerare i test di integrazione. Prova il risultato richiesto e non l'operazione di caricamento del file.

Per unit test, suggerirei di includere il file di test nel progetto (file EAR o equivalente), quindi utilizzare un percorso relativo nei test unitari, ad es. " ../ testdata / testfile " ;.

Finché il progetto viene esportato / importato correttamente, il test unitario dovrebbe funzionare.

Come altri hanno già detto, il primo va bene come test di integrazione. Il secondo verifica solo ciò che la funzione dovrebbe effettivamente fare, che è tutto ciò che un test unitario dovrebbe fare.

Come mostrato, il secondo esempio sembra un po 'inutile, ma ti dà l'opportunità di testare come la funzione risponde agli errori in uno qualsiasi dei passaggi. Nell'esempio non si verifica alcun errore, ma nel sistema reale si potrebbe avere e l'iniezione di dipendenza consente di testare tutte le risposte a eventuali errori. Quindi ne sarà valsa la pena.

Autorizzato sotto: CC-BY-SA insieme a attribuzione
Non affiliato a StackOverflow
scroll top