Programming to Abstractions is a another common architectural pattern that facilitates unit testing. Abstractions provide a way for us to decouple specific implementation details from the consuming class. Another benefit of abstractions is that we can substitute alternate implementations if needed – something that is very difficult to do otherwise.
Let’s start by looking at our trusty ReallyHardToTest class that we’ve been working with:
Let’s focus on the ReallyHardToTest constructor, which takes an instance of SomeOtherDependency. We know from prior tutorials that we can’t mock concrete types, but we want to make sure that the ReallyHardToTest constructor invokes SomeOtherDependency.DoSomething(). So how can we make this more testable?
Abstractions to the Rescue!
There are a couple of potential solutions, the simplest of which would be if SomeOtherDependency already implemented an interface. In that case we would just have to update the constructor to take the interface (abstraction) instead of the concrete type.
In our scenario though, SomeOtherDependency does not yet implement any interface, so let’s fix that up!
Nothing particularly fancy here – We created a new interface on line 25, then attached it to SomeOtherDependency on line 29, so now SomeOtherDependency ‘is-a’ ISomeOtherDependency.
Back in the ReallyHardToTest class, we update the constructor to take ISomeOtherDependency instead of SomeOtherDependency, and that’s that!
So now that we’ve abstracted SomeOtherDependency, we can make our tests cleaner and even add a new one:
SWEET, now we’ve got 2 tests working! Let’s dig into the changes:
On line 7, we’re following the same pattern as IPathWrapper, declaring a Mock for our dependency. Likewise, on line 14 we instantiate that mock.
Things get a little weirder around line 15 – Why are we configuring the mock here instead of in our test like before? The answer is pretty simple: We invoke SomeOtherDependency.DoSomething in the ReallyHardToTest constructor itself, so the mock must be configured before SystemUnderTest can be constructed. If we don’t have that configuration in place, the test will fail as soon as we create SystemUnderTest.
And then on line 33, we have our new test, which verifies that the constructor does its expected work. This one looks pretty weird though – there’s no Arrange or Act to speak of, all we’re doing is verifying our mocks. What gives?
As I noted in the comments, the “Arrange” phase for this test is taken care of in the SetUp method. The same goes for the “Act” phase – we’re testing instantiation logic. All that remains, then, is the “Assert” phase, verifying that expectations were met.
Conclusion and Next Steps:
We’ve been covering some fairly complex topics lately, so I decided to take it easy on you with this one 🙂 Today we saw how easy it is to introduce an abstraction for code that we own so we can make it more testable. The basic process is:
- Extract an interface from the concrete dependency (Visual Studio has tools to do this automatically if you want)
- Attach (implement) that interface on your concrete class
- Update the dependent class to depend upon the interface rather than the concrete type
- Update your test class to use a mock in place of the concrete dependency
We also touched briefly on some unusual aspects of testing constructors:
- Constructor tests typically lack an Arrange or Act step
- Configuration for mocks used in the constructor must take place prior to instantiating the System Under Test
In our next installment, let’s tackle that nasty “var model = new SomeModel()” bit!
Bonus Fanciness: Dynamic Programming!
Abstractions aren’t just for testing – another common use for abstractions is to provide dynamic behavior at runtime. For example, let’s look at this simple system:
There’s nothing really earth-shattering here, but note that we have two implementations of ILogger: ConsoleLogger and BirthdayConsoleLogger. We also see from the SomethingThatUsesLoggers constructor that it takes a dependency on ILogger. Since ILogger is an abstraction, we can inject either a ConsoleLogger or a BirthdayConsoleLogger into SomethingThatUsesLoggers and it will work just the same.
The power of this approach is that we can change our application’s behavior based on runtime conditions (i.e. is it the user’s birthday today?), dynamically substituting one implementation for another. Cool, huh?
Next up, though, let’s look at how to remove that ‘new SomeModel’ call in our ReallyHardToTest constructor.