In Chapter 3 of his book The Art of Unit Testing: with Examples in C#, Roy Osherove illustrates the issue of undesirable external dependencies in code under test.
He shows this with a method named IsValidLogFileName
which takes in a filename string as an argument, reads in a config file from the local filesystem and checks whether the given filename extension exists in the config file. Here's what such a class would look like:
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; } }
The issue here is that testing IsValidLogFileName
has a dependency on the local filesystem.
The author introduces the concept of stubs as a means of removing this dependency. In particular, he describes the following design:
Create an interface:
interface IExtensionManager { bool IsValid(string fileName); }
an implementing production class:
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; } }
a 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
as follows:
public class LogAnalyzer { IExtensionManager em; public LogAnalyzer(IExtensionManager extensionManager) { em = extensionManager; } public bool IsValidLogFileName(string fileName) { return em.IsValid(fileName); } }
run production code like this:
LogAnalyzer la = new LogAnalyzer(new FileExtensionManager);
and test code like this:
LogAnalyzer la = new LogAnalyzer(new FakeExtensionManager);
The part I don't understand is why we have to extract the validation logic
from the original IsValidLogFileName
by making an ExtensionManager
interface.
Wouldn't it make more sense to create something like a ConfigManager
interface
and refactor as follows:
interface:
interface IConfigManager { string [] GetFileExtensionConfig(); }
an implementing production class:
class LocalConfigManager : IConfigManager { string [] GetFileExtensionConfig( ) { return System.IO.File.ReadAllLines(@"C:\Users\Public\ext.config"); } }
a stub:
class FakeConfigManager : IConfigManager { string [] GetFileExtensionConfig( ) { string [] extensions = { "txt", "c", "cpp", "cs"}; return extensions; } }
refactor LogAnalyzer
as follows:
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; }
run production code like this:
LogAnalyzer la = new LogAnalyzer(new LocalConfigManager);
and test code like this:
LogAnalyzer la = new LogAnalyzer(new FakeConfigManager);