Search code examples
c#unit-testingdependency-injectionnunit

How to inject a fake without disrupting the code being tested


the following is the original text of the book 《The Art of Unit Testing, Second Edition》: Extract an interface to allow replacing underlying implementation

  • In this technique, you need to break out the code that touches the filesystem into a separate class. That way you can easily distinguish it and later replace the call to that class from your tests (as was shown in figure 3.3). This first listing shows the places where you need to change the code.

Listing 3.1 Extracting a class that touches the filesystem and calling it

public bool IsValidLogFileName(string fileName)
{
 FileExtensionManager mgr =
 new FileExtensionManager();
 return mgr.IsValid(fileName);
}
class FileExtensionManager
 {
 public bool IsValid(string fileName)
 {
 //read some file here
 }
}
  • Next, you can tell your class under test that instead of using the concrete FileExtensionManager class, it will deal with some form of ExtensionManager, without knowing its concrete implementation. In.NET, this could be accomplished by either using a base class or an interface that FileExtensionManager would extend.The next listing shows the use of a new interface in your design to make it more testable. Figure 3.4 showed a diagram of this implementation.

Listing 3.2 Extracting an interface from a known class

public class FileExtensionManager : IExtensionManager
 {
 public bool IsValid(string fileName)
 {
 //read some file here
 }
 }
public interface IExtensionManager
 {
 bool IsValid (string fileName);
 }
//the unit of work under test:
public bool IsValidLogFileName(string fileName)
{
 IExtensionManager mgr =
 new FileExtensionManager();
 return mgr.IsValid(fileName);
 }

Note that 'IExtensionManager mgr = new FileExtensionManager();' in the 'IsValidLogFileName' method is causing issues later on.

Listing 3.4 Injecting your stub using constructor injection

public class LogAnalyzer
{
    private IExtensionManager manager;
    public LogAnalyzer(IExtensionManager mgr)
    {
    manager = mgr;
    }
    public bool IsValidLogFileName(string fileName)
    {
    return manager.IsValid(fileName);  //The tested code has been disrupted; 'IExtensionManager mgr = new FileExtensionManager();' has been deleted!!!!!
    }
}
public interface IExtensionManager
{
    bool IsValid(string fileName);
}


[TestFixture]
public class LogAnalyzerTests
{
    [Test]
    public void
    IsValidFileName_NameSupportedExtension_ReturnsTrue()
    {
    FakeExtensionManager myFakeManager =
    new FakeExtensionManager();
    myFakeManager.WillBeValid = true;
    LogAnalyzer log =
    new LogAnalyzer (myFakeManager);
    bool result = log.IsValidLogFileName("short.ext");
    Assert.True(result);
    }
    }
internal class FakeExtensionManager : IExtensionManager
{
    public bool WillBeValid = false;
    public bool IsValid(string fileName)
    {
    return WillBeValid;
    }
}

Please note the IsValidLogFileName method above. Although the modifications allow the test code to run correctly, but the tested code has been disrupted; 'IExtensionManager mgr = new FileExtensionManager();' has been deleted.

This disrupts the integrity of the source code functionality. When we run the source code normally, the program will not be able to call the IsValid method of the FileExtensionManager class correctly! You will not get the same result as shown in Listing 3.2. If I want to restore the source code, where should I place 'IExtensionManager mgr = new FileExtensionManager();' in line 3.4?So that the code calls the IsValid method of the class FileExtensionManager during runtime and the IsValid method of the class FakeExtensionManager during testing


Solution

  • Whatever you're using to create a new instance of the LogAnalyzer should also create the appropriate instance of a class that implements IExtensionManager and inject it via the constructor.

    Just like your tests are instantiating and injecting a fake, your real code should instantiate and inject a real instance. This could be using an IoC container or 'Pure DI'.

    To answer the question:

    ...where should I place 'IExtensionManager mgr = new FileExtensionManager()?

    You do this in your 'Composition Root'.