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);