Search code examples
c#unit-testingdesign-patternsjunitnunit

Why can I replace the underlying implementation after extracting the interface?


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)
 {
 ...
 }
 }
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);
 }
  • You create an interface with one IsValid (string) method and make FileExtensionManager implement that interface. It still works exactly the same way, only now you can replace the “real” manager with your own “fake” manager, which you’ll create later to support your test.You still haven’t created the stub extension manager, so let’s create that right now.It’s shown in the following listing

Question: I don’t understand why adding an interface makes it easier to distinguish and replace class calls later? Is this question in the scope of design patterns? Should I learn some design patterns to better understand this question?


Solution

  • I don’t understand why adding an interface makes it easier to distinguish and replace class calls later?

    That's an understandable confusion, given the limited example. The IsValidLogFileName method is still tightly coupled to the FileExtensionManager because it's calling its constructor.

    In both the first and second example, you'd need to change one line of code if you wanted to change to a different type of extension manager. So it doesn't seem to make much of a difference.

    Is this question in the scope of design patterns? Should I learn some design patterns to better understand this question?

    Yes. Look into Dependency Injection. If IsValidLogFileName used an IExtensionManager object that was given to it, rather than creating a new FileExtensionManager() directly, then you would totally eliminate the reference to FileExtensionManager from its code.

    You could replace the FileExtensionManager with a BlobExtensionManager or a FakeExtensionManager or an EmptyExtensionManager by simply passing a different object in. Your method's code wouldn't have to change at all!

    You could write unit tests that use a mock IExtensionManager to specify what IsValid should return for different values, to verify that IsValidLogFileName behaves the way it should behave in a variety of circumstances.

    Using an interface the way it's given in the example is just taking one step closer to decoupling yourself from implementation details. You won't see the full benefits until you can remove the implementation reference more completely from your method and its class.