Correggere Seam per Stub

0

Nel capitolo 3 del suo libro The Art of Unit Test: con esempi in C # , Roy Osherove illustra la questione di indesiderabili dipendenze esterne nel codice sotto test.

Lo mostra con un metodo chiamato IsValidLogFileName che accetta una stringa di nome file come argomento, legge un file di configurazione dal file system locale e controlla se l'estensione del nome file esiste nel file di configurazione. Ecco come sarebbe una classe del genere:

   public class LogAnalyzer
    {
        public bool IsValidLogFileName(string fileName)
        {

            string [] extensions = System.IO.File.ReadAllLines(@"C:\Users\Public\ext.config");

            foreach (string ext in extensions)
            {
                if (fileName.EndsWith(ext))
                {
                    return true;
                }
            }
            return false;
        }
    }

Il problema qui è che testare IsValidLogFileName ha una dipendenza dal filesystem locale.

L'autore introduce il concetto di stub come mezzo per rimuovere questa dipendenza. In particolare, egli descrive il seguente disegno:

Crea un'interfaccia:

    interface IExtensionManager
    {
        bool IsValid(string fileName);
    }

una classe di produzione attuativa:

    class FileExtensionManager : IExtensionManager
    {
        bool IsValid(string fileName)
        {
            string [] extensions = System.IO.File.ReadAllLines(@"C:\Users\Public\ext.config");

            foreach (string ext in extensions)
            {
                if (fileName.EndsWith(ext))
                {
                    return true;
                }
            }
            return false;
        }
    }

uno stub:

    class FakeExtensionManager : IExtensionManager
    {
        bool IsValid(string fileName)
        {
            string [] extensions = { "txt", "c", "cpp", "cs"};

            foreach (string ext in extensions)
            {
                if (fileName.EndsWith(ext))
                {
                    return true;
                }
            }
            return false;
        }
    }

refactor LogAnalyzer come segue:

   public class LogAnalyzer
    {
        IExtensionManager em;

        public LogAnalyzer(IExtensionManager extensionManager)
        {
            em = extensionManager;
        }

        public bool IsValidLogFileName(string fileName)
        {
            return em.IsValid(fileName);
        }
    }

eseguire il codice di produzione come questo:

    LogAnalyzer la = new LogAnalyzer(new FileExtensionManager);

e codice di prova come questo:

    LogAnalyzer la = new LogAnalyzer(new FakeExtensionManager);

La parte che non capisco è il motivo per cui dobbiamo estrarre la logica di validazione dall'originale IsValidLogFileName creando un'interfaccia ExtensionManager . Non avrebbe più senso creare qualcosa come un'interfaccia ConfigManager e refactoring come segue:

interfaccia:

    interface IConfigManager
    {
        string [] GetFileExtensionConfig();
    }

una classe di produzione attuativa:

    class LocalConfigManager : IConfigManager
    {
        string [] GetFileExtensionConfig( )
        {
            return System.IO.File.ReadAllLines(@"C:\Users\Public\ext.config");
        }
    }

uno stub:

    class FakeConfigManager : IConfigManager
    {
        string [] GetFileExtensionConfig( )
        {
            string [] extensions = { "txt", "c", "cpp", "cs"};
            return extensions;
        }
    }

refactor LogAnalyzer come segue:

   public class LogAnalyzer
    {
        IConfigManager cm;

        public LogAnalyzer(IConfigManager configManager)
        {
            cm = configManager;
        }

        public bool IsValidLogFileName(string fileName)
        {
            string [] extensions = cm.GetFileExtensionConfig( );

            foreach (string ext in extensions)
            {
                if (fileName.EndsWith(ext))
                {
                    return true;
                }
            }
            return false;
    }

eseguire il codice di produzione come questo:

    LogAnalyzer la = new LogAnalyzer(new LocalConfigManager);

e codice di prova come questo:

    LogAnalyzer la = new LogAnalyzer(new FakeConfigManager);

    
posta Isaac Kleinman 07.03.2014 - 17:05
fonte

2 risposte

3

Roy qui (autore del libro). Mi piace la tua implementazione di risolvere questo problema. Per prima cosa, astrae solo la parte IO, e non la logica, lasciando la logica in una posizione testabile. Secondo, nella tua implementazione la classe LogAnalyzer fa ancora qualcosa. Nella mia implementazione nel libro, dopo il refactoring, LogAnalyzer non fa altro che essere un meccanismo di inoltro verso il manager di Extension.

Una cosa da notare nella tua spiegazione originale di ciò che mostro nel libro:

Non consiglio affatto che il gestore di estensioni fittizio faccia alcuna logica, ma invece preleva le code dal test per sapere cosa restituire. Quindi la logica non è implementata due volte (nel codice prod e falso).

In sostanza, hai avuto l'idea. Fake fuori il codice di dipendenza. È una buona implementazione della soluzione per un pezzo di codice così piccolo e semplice e non avrei alcun problema se ti vedessi farlo nel codice di produzione del mondo reale. Probabilmente direi "Bel lavoro!".

Quindi continuerei a dire che la configurazione dell'estensione fasulla non dovrebbe avere estensioni hard coded, ma dovrebbe essere configurata dal test in modo che:

  1. il test è più leggibile
  2. la classe falsa è più riutilizzabile in altri test in cui potresti voler simulare altre configurazioni

    FakeConfigMgr fakeConfig  = new FakeConfigMgr();
    fakeConfig.ValidExtensionsWillBe = new []("txt","ext");
    LogAnalyzer la = new LogAnalyzer(fakeConfig);
    
risposta data 08.03.2014 - 06:56
fonte
2

Lo fa, e questo tipo di cose è approssimativamente ciò che Dependency Injection è tutto, ma ... come hai visto, inizia a diventare davvero complesso anche per una semplice e semplice funzione che stai descrivendo. Immagina come sarebbe se ogni classe in un grande programma fosse resa così complessa!

Ora non è così male usare DI in pratica perché tendi ad usarlo per certi livelli di configurazione e hanno strumenti che ti aiutano a gestirlo tutto, ma è ancora più complesso della semplice scrittura del codice.

Il problema è che non puoi semplicemente sostituire la chiamata System.IO.File.ReadAllLines nei tuoi test usando gli strumenti di test dell'unità tradizionali. Idealmente, dovresti scrivere un test e dire semplicemente al compilatore di utilizzare temporaneamente una chiamata ReadAllLines diversa. Si scopre che è possibile farlo, utilizzare Fake da Microsoft . Ciò utilizza un approccio di basso livello per riscrivere il codice compilato (in un modo che ricorda gli hacker che sostituiscono le chiamate per copiare la protezione senza nulla). Quindi puoi letteralmente sostituire le chiamate di sistema nei tuoi test. L'esempio classico sta sostituendo le chiamate a DateTime.Now () con qualunque cosa desideri che sia l'ora corrente.

Francamente, eliminerei tutti i tuoi strumenti di test per la creazione di stub e userei Fakes su tutta la linea. Gli strumenti di vecchia tecnologia basati su interfacce sono obsoleti e devono morire, quindi è possibile scrivere il codice nel modo in cui è stato avviato (semplice e semplice) senza doverlo manipolare artificialmente solo per compiacere gli strumenti. Fakes ti offre tutti i vantaggi del codice semplice e pulito con tutti i vantaggi dei test di unità isolate!

    
risposta data 07.03.2014 - 17:13
fonte

Leggi altre domande sui tag