Search code examples
c#.netunit-testingoopisolation-frameworks

Is faking dependencies with interfaces or virtual methods a good design choice?


I am trying to learn unit testing but there is a design issue resulting from it. Consider class A has a dependency on class B. If you want to create a stub for B in order to unit test A, most isolation frameworks require that B has to be an interface or all the methods used by A must be virtual. B can't be a concrete class with non-virtual methods in essence in order to unit test.

This imposes major restrictions on the design of production code. If I have to create an interface for every dependency then number of classes will double. Following Single Responsibility Principle leads to small classes that depend on each other so this will blow up the number of interfaces. Also I create interfaces for volatile dependencies(likely to change in the future) or if the design requires it for extensibility. Polluting the production code with interfaces just for testing will increase its complexity significantly. Making all methods virtual doesn't seem to be a good solution either. It gives inheritors the impression that these methods are ok to be overridden even if they aren't and in reality this is just a side effect of unit testing.

Does this mean that testable object oriented design doesn't allow concrete dependencies or does it mean that concrete dependencies shouldn't be faked? "Every dependency must be faked(stub or mock) to unit test correctly" is what I learned so far so I don't think latter is the case. Isolation frameworks other than JustMock and Isolator doesn't allow faking concrete dependencies without virtual methods and some argue that power of JustMock and Isolator lead to bad design. I think that the ability to mock any class is very powerful and it will keep the design of production code clean if you know what you are doing.


Solution

  • I realized later that this question also asks the same and there seems to have no solution. Choosing between creating an interface or making all methods virtual is a restriction of C# which is statically typed language. Duck-typed language such as Ruby doesn't impose this and a fake object can easily be created without changing the original class. In Ruby fake object just needs to create the appropriate methods and it can be used instead of the original dependency.

    Edit:

    I finished reading the book The Art of Unit Testing by Roy Osherove and found that the following paragraphs are related:

    Testable designs usually only matter in static languages, such as C# or VB.NET, where testability depends on proactive design choices that allow things to be replaced. Designing for testability matters less in more dynamic languages, where things are much more testable by default. In such languages, most things are easily replaceable, regardless of the project design. This rids the community of such languages from the straw-man argument that the lack of testability of code means it’s badly designed and lets them focus on what good design should achieve, at a deeper level.

    Testable designs have virtual methods, nonsealed classes, interfaces, and a clear separation of concerns. They have fewer static classes and methods, and many more instances of logic classes. In fact, testable designs correlate to SOLID design principles but don’t necessarily mean you have a good design. Perhaps it’s time that the end goal should not be testability but good design alone.

    This basically means that making the design testable because of the restrictions of a static language doesn't make it a "good design" inherently. For me a good design accomplishes what is needed for today's requirements and doesn't think about future too much. Of course making every dependency abstract is good for maintainability in the future but it makes the API very complex. I want to make a dependency an interface if its likely to change or many concrete classes implement that interface not because testability requires it. Doing this because testability requires it leads to "bad design".