I / O file di test unità

Leggendo i thread relativi ai test unitari esistenti qui su Stack Overflow, non sono riuscito a trovarne uno con una risposta chiara su come eseguire operazioni di I / O su file di test unitario. Solo di recente ho iniziato a esaminare i test delle unità, essendo stato precedentemente consapevole dei vantaggi ma avendo difficoltà ad abituarmi a scrivere i test per primi. Ho impostato il mio progetto per utilizzare NUnit e Rhino Mocks e sebbene capisco il concetto che sta dietro, ho qualche problema a capire come usare Mock Objects.

Nello specifico ho due domande a cui vorrei rispondere. Innanzitutto, qual è il modo corretto per eseguire operazioni di I / O su file di test? In secondo luogo, nei miei tentativi di apprendere i test unitari, mi sono imbattuto in un’iniezione di dipendenza. Dopo aver impostato e lavorato Ninject, mi chiedevo se dovessi usare DI nei test di unità, o solo istanziare direttamente gli oggetti.

Scopri Tutorial su TDD usando Rhino Mocks e SystemWrapper .

SystemWrapper include molte delle classi System.IO tra cui File, FileInfo, Directory, DirectoryInfo, …. Puoi vedere la lista completa .

In questo tutorial sto mostrando come eseguire test con MbUnit ma è esattamente lo stesso per NUnit.

Il test avrà un aspetto simile a questo:

[Test] public void When_try_to_create_directory_that_already_exists_return_false() { var directoryInfoStub = MockRepository.GenerateStub(); directoryInfoStub.Stub(x => x.Exists).Return(true); Assert.AreEqual(false, new DirectoryInfoSample().TryToCreateDirectory(directoryInfoStub)); directoryInfoStub.AssertWasNotCalled(x => x.Create()); } 

Non c’è necessariamente una cosa da fare quando si verifica il file system. In verità, ci sono molte cose che potresti fare, a seconda delle circostanze.

La domanda che devi porre è: cosa sto testando?

  • Che il file system funziona? Probabilmente non hai bisogno di testarlo a meno che tu non stia usando un sistema operativo che non hai molta familiarità con. Quindi, se stai semplicemente dando un comando per salvare i file, ad esempio, è una perdita di tempo scrivere un test per assicurarti che risparmi davvero.

  • Che i file vengano salvati nel posto giusto? Bene, come fai a sapere qual è il posto giusto? Presumibilmente hai un codice che combina un percorso con un nome di file. Questo è il codice che puoi testare facilmente: il tuo input è di due stringhe e l’output dovrebbe essere una stringa che è una posizione di file valida costruita usando queste due stringhe.

  • Che ottieni il giusto set di file da una directory? Probabilmente dovrai scrivere un test per la tua class getter di file che testa realmente il file system. Ma dovresti usare una directory di test con i file che non cambieranno. Dovresti inserire questo test anche in un progetto di test di integrazione, perché questo non è un vero test di unità, perché dipende dal file system.

  • Ma, ho bisogno di fare qualcosa con i file che ottengo. Per quel test, dovresti usare un falso per la tua class getter di file. Il tuo falso dovrebbe restituire un elenco di file hard-coded. Se si utilizza un vero getter di file e un vero processore di file, non si saprà quale causa causa un errore di test. Quindi la class del processore di file, in fase di test, dovrebbe utilizzare una class di getter di file falsi. La class del file-processor dovrebbe utilizzare l’ interfaccia getter del file. Nel codice reale, passerai nel vero file-getter. Nel codice di test si passa un falso file getter che restituisce un elenco statico noto.

I principi fondamentali sono:

  • Usa un file system falso, nascosto dietro un’interfaccia, quando non stai testando il file system stesso.
  • Se hai bisogno di testare le operazioni sui file reali, allora
    • contrassegnare il test come test di integrazione, non come test unitario.
    • avere una directory di test designata, un set di file, ecc. che sarà sempre lì in uno stato invariato, in modo che i test di integrazione orientati ai file possano passare in modo coerente.

Q1:

Hai tre opzioni qui.

Opzione 1: vivi con esso.

(nessun esempio: P)

Opzione 2: crea una leggera astrazione dove richiesto.

Invece di eseguire il file I / O (File.ReadAllBytes o qualsiasi altra cosa) nel metodo in prova, è ansible cambiarlo in modo che l’IO venga eseguito all’esterno e al suo posto venga trasmesso un stream.

 public class MyClassThatOpensFiles { public bool IsDataValid(string filename) { var filebytes = File.ReadAllBytes(filename); DoSomethingWithFile(fileBytes); } } 

potrebbe diventare

 // File IO is done outside prior to this call, so in the level // above the caller would open a file and pass in the stream public class MyClassThatNoLongerOpensFiles { public bool IsDataValid(Stream stream) // or byte[] { DoSomethingWithStreamInstead(stream); // can be a memorystream in tests } } 

Questo approccio è un compromesso. Innanzitutto, sì, è più testabile. Tuttavia, scambia testabilità per una leggera aggiunta alla complessità. Questo può influire sulla manutenibilità e sulla quantità di codice che devi scrivere, inoltre puoi spostare il tuo problema di test su un solo livello.

Tuttavia, nella mia esperienza, questo è un approccio gradevole e bilanciato, poiché è ansible generalizzare e rendere testabile la logica importante senza impegnarsi in un file system completo. Per esempio puoi generalizzare i pezzi che ti interessano davvero, lasciando il resto così com’è.

Opzione 3: avvolgere l’intero file system

Facendo un ulteriore passo avanti, prendere in giro il filesystem può essere un approccio valido; dipende da quanto sei gonfio con cui vuoi vivere.

Ho già seguito questa strada; Avevo implementato un file system incartato, ma alla fine l’ho appena cancellato. C’erano delle sottili differenze nell’API, dovevo iniettarlo dappertutto e alla fine era un dolore extra per poco guadagno dato che molte delle classi che lo usavano non erano estremamente importanti per me. Se stavo usando un contenitore IoC o scrivendo qualcosa che era fondamentale e i test dovevano essere veloci, avrei potuto rimanere bloccato con esso. Come con tutte queste opzioni, il tuo chilometraggio può variare.

Come per la tua domanda sul contenitore IoC:

Inietti il ​​tuo test raddoppia manualmente. Se devi fare un sacco di lavoro ripetitivo, basta usare i metodi setup / factory nei test. L’utilizzo di un contenitore IoC per i test sarebbe eccessivo in casi estremi! Forse non sto capendo la tua seconda domanda, però.

Attualmente, consumo un object IFileSystem tramite l’integrazione delle dipendenze. Per il codice di produzione, una class wrapper implementa l’interfaccia, avvolgendo specifiche funzioni IO di cui ho bisogno. Durante il test, posso creare un’implementazione null o stub e fornirla alla class sottoposta a test. La class testata non è la più saggia.

Dal 2012, puoi farlo usando Microsoft Fakes senza la necessità di cambiare il tuo codice base, ad esempio perché era già stato congelato.

In primo luogo generare un assembly falso per System.dll – o qualsiasi altro pacchetto e quindi prendere in giro i rendimenti attesi come in:

 using Microsoft.QualityTools.Testing.Fakes; ... using (ShimsContext.Create()) { System.IO.Fakes.ShimFile.ExistsString = (p) => true; System.IO.Fakes.ShimFile.ReadAllTextString = (p) => "your file content"; //Your methods to test }