Come posso testare unitamente la presenza di specifici contenuti di file, attraverso un'interfaccia?

1

Attualmente sto provando a testare una classe di file manager. Questa classe è responsabile di tenere traccia di quanto a lungo il file è buono. Il file avrà una data scritta che indica quando è stato generato e la durata della durata del file. I consumatori della mia interfaccia saranno agnostici a questi dettagli. L'interfaccia ha il seguente aspetto:

public interface IFileManager
{
    bool GenerateFile(string location);
    bool DeleteFile(string location);
    bool IsFileValid(string location);
    bool Update(string customerID, string location);
}

Mi preoccupo di come testare uno di questi metodi, in particolare IsValid(string location) . Parte dell'implementazione che sto pianificando per IsValid sarà quella di verificare se il file è scaduto, così come il file che non contiene solo spazzatura.

Ok, ecco il mio enigma. Scrivo sempre i miei test unitari in un progetto separato, in cui hanno solo un riferimento a una libreria di interfaccia. Cioè, eseguo il test di tutto tramite un'interfaccia in modo che i miei test di unità conoscano solo IFileManager . Quindi, da un'interfaccia di alto livello, come posso testare un'implementazione specifica di un file "scaduto" o "spazzatura"?

La mia interfaccia non consente a un client di detta interfaccia di creare un file che è già scaduto. E questo ha senso per me! Inoltre, non voglio aggiungere funzionalità alla mia interfaccia solo per essere in grado di testare! I miei pensieri su come posso testare questo, mi danno un odore di codice.

    
posta Snoop 19.06.2018 - 16:46
fonte

6 risposte

5

My thoughts about how can I test this, give me a code smell.

Questa API sta incapsulando un effetto collaterale. E va bene - la maggior parte del tuo codice non ha bisogno di conoscere i dettagli. Ma per il test delle unità, probabilmente hai bisogno di un modo per sostituire l'effetto collaterale con un doppio di prova.

La tua implementazione sta probabilmente utilizzando un numero limitato di chiamate a qualche libreria di file per eseguire il lavoro effettivo. Quindi l'idea è di avere un'interfaccia che avvolga quelle chiamate e passare un'istanza di tale interfaccia per ottenere il IFileManager desiderato.

interface FilePrimitives { ... }

IFileManager fileManager(FilePrimitives filePrimitives) { ... }

In pratica è il modello "Strategia" al lavoro.

Nel codice di produzione, si inizializza FileManager utilizzando un'implementazione di FilePrimitives che effettua chiamate di libreria effettive. I primitivi fanno effettivamente parte della "shell imperativa", in cui il tuo codice interagisce con il mondo esterno.

Punto chiave: uno dei tuoi vincoli nell'implementazione dei primitivi è che dovrebbero essere troppo semplici da interrompere.

Quando esegui dei test, invece di usare le primitive "reali", usi un test double e controlli le risposte dal test.

Quindi, se vuoi testare un file spazzatura, devi impostare il test double in modo che risponda come se ci sia davvero un file spazzatura, e poi usarlo per inizializzare il resto del tuo sistema .

Il tuo test è veramente due pezzi - è un validatore di use case (che esercita l'API), ma è anche un composizione radice .

So you are saying to abstract the file library and then inject a mock version of it that always reports that a file is expired?

Sì, anche se lo scriverei in un modo un po 'diverso: stiamo prendendo il decision per utilizzare questa libreria di file e posizionare un limite del modulo attorno a tale decisione, in modo da poter cambiare tale decisione nel cablaggio di prova.

    
risposta data 19.06.2018 - 17:04
fonte
4

Invece di chiamare un metodo di sistema statico per ottenere l'ora corrente, iniettare l'origine del tempo come dipendenza. Quindi prendi in giro la sorgente del tempo per simulare il tempo che passa. Ecco un codice Java:

class FileManager {
    private Supplier<Instant> timeSource;

    public FileManager() {
        this(() -> Instant.now());
    }

    public FileManager(Supplier<Instant> timeSource) { 
         this.timeSource = timeSource;
    }

    public GenerateFile() {
        Instant currentTime = timeSource.get();
        ...
    }
    public IsFileValid() {
      Instant currentTime = timeSource.get();
      ...
    }
}

Ora usa la tua libreria di derisione preferita per creare un'origine temporale fittizia:

public class FileManagerTest() {
     public void WHEN_file_expired_THEN_not_valid() {
          when(mockTimeSource.get()).thenReturn(someTime)
                                   .thenReturn(aLaterTime);
          FileManager manager = new FileManager(mockTimeSource);
          GenerateFile(...);
          assert(! IsFileValid(...));
     }
}
    
risposta data 19.06.2018 - 18:23
fonte
3

Se stai testando l'implementazione dell'interfaccia, avrai una classe concreta, implementando l'interfaccia, e i test delle unità saranno in grado di accedere a questa classe.

Se stai testando le classi che hanno IFileManager come dipendenza, quindi crea uno stub / mock che implementa IFileManager e, quando chiama IsFileValid restituisce sempre true o sempre false o un valore che sarà configurato in ogni test.

    
risposta data 19.06.2018 - 17:03
fonte
0

I always write my unit tests in a separate project, where they only have a reference to an interface library. That is, I test everything through an interface so my unit tests will only know about IFileManager

Sto ripetendo almeno una risposta, ma mi piacerebbe anche enfatizzare ulteriormente, che questo approccio citato non mi sembra giusto. È possibile utilizzare l'astrazione di una dipendenza durante il test unitario del codice derivato, questo è il seguente: è possibile iniettare un'imitazione fittizia di IFileManager per testare un codice che utilizza FileManager. Ma per testare il FileManager stesso si prova la classe concreta. Limitare l'interfaccia non è ancora così male, semplicemente non porta alcun beneficio. E quando il test ha bisogno di utilizzare alcuni dettagli privati, come per simulare la funzione del tempo, inizia ad essere un ostacolo.

    
risposta data 20.06.2018 - 06:11
fonte
0

Presumo che tu conosca la classe di implementazione che stai provando a testare o almeno tutte le informazioni su come è implementata.

Nella tua classe di test devi solo istanziare la classe di implementazione:

IFileManager myInstance = new implementingClass();
// prepare the file to test
...
bool result = myInstance.IsFileValid(@"SomeLocation");
// process result
...

Se vuoi diventare veramente generico, puoi usare reflection per ottenere la classe (o le classi) che implementano l'interfaccia e usa Invoke per chiamare il costruttore della classe.

// Loop through the .DLLs where the implementation is kept.
// Test that it contains the IFileManager interface
...
ConstructorInfo ci = myModule.GetType().GetInterface(@"IFileManager").GetConstructorInfo(new Type[0]);
// The above needs error handling and validation, an exercise to do.
// Instantiate the class that implemented IFileManager
IFileManager ifm = ci.Invoke(new Object[0]);
// Now perform you tests by calling the methods:
ifm.IsFileValid(@"SomeFile");
...

Questa è una grossolana semplificazione dei passaggi ma l'idea chiave è lì. Usa reflection per ottenere tutte le DLL che implementano la tua interfaccia, quindi testarle istanziando ogni classe e testando l'implementazione.

    
risposta data 19.06.2018 - 20:57
fonte
0

Direi che a livello di interfaccia, un test unitario di IsFileValid probabilmente non ha senso.

Il fatto che la validità del file sia determinata dall'ora corrente rispetto a un intervallo di tempo nel file è una parte definita dell'interfaccia? È impossibile avere diverse implementazioni di IFileManager che determinano la validità di qualche altra metrica, come il numero di versione?

Se è così, allora va bene, puoi testarlo (anche se al punto che i dettagli di implementazione sono definiti in tale misura stai perdendo parte del punto di avere un'interfaccia). In caso contrario, cosa testerebbe esattamente un test unitario di IsFileValid? Che ritorna vero o falso? Perché uno potrebbe essere un output valido per un determinato file in alcune possibili implementazioni.

Se per una specifica implementazione concreta, la validità del file è per definizione basata su un intervallo di tempo come indicato sopra, quindi testare l'unità dell'implementazione concreta.

    
risposta data 21.06.2018 - 00:23
fonte

Leggi altre domande sui tag