Challenge: Classes with Static Dependencies are Difficult to Test
Static Dependencies pose a set of challenges to testability, most notably:
- Static Dependencies cannot be mocked
- Due to their nature, static dependencies are rarely injected so they are hidden dependencies
In my prior post, I showed a very difficult-to-test class that uses the static System.IO.Path.GetFileName method. Today, we’ll do some refactoring on that class to make it more testable!
For reference, here is the class in its original form:
Round 1: Implement a Wrapper for Static Dependencies
Let’s start with the SetFilename method first. As mentioned in the previous article, we can work around static dependencies by implementing a wrapper class:
Effectively, all we did was create an interface that models the methods from System.IO.Path that we need, then implement that interface on our wrapper class. Once we wire up this new dependency we will be able to mock IPathWrapper for tests.
But PathWrapper is still using a static class, how can we test that?
Good eye! You’re correct that we still cannot test PathWrapper itself, but at least we have isolated the non-testable aspect from the remainder of the application.
A common convention on TDD teams is to avoid testing pure wrappers (i.e. Classes that only ‘wrap’ .NET framework elements without adding behavior) because doing so is equivalent to unit testing the .NET framework itself. TDD often involves pragmatic decisions like this – ultimately, the goal is to find a reasonable cutoff point and test everything else.
So let’s go wire up this awesome new dependency and write a test!
Round 2: Inject the Wrapper Class
Let’s go back to the ReallyHardToTest class and update it to take a constructor dependency on IPathWrapper:
Here’s a quick summary of the changes we made:
- Added a constructor dependency on IPathWrapper
- Stored the injected IPathWrapper so we can use it later
- Updated SetFilename to use the IPathWrapper instance instead of System.IO.Path
Round 3: Write a Test
Ok, so now let’s go write a test!
Wow, that looks like an awful lot of code…
Actually, the majority of the code is NUnit boilerplate, the key is in the SetUp and SetFilename_Always_InvokesPathGetFileName methods, so let’s take a look at them…
SetUp gets executed before each test runs, so it’s a great place to create our dependencies and instantiate the System Under Test. In our SetUp method, we are:
- Creating a mock instance of our IPathWrapper component
- Creating a concrete instance of SomeOtherDependency
- Creating a concrete instance of ReallyHardToTest (our System Under Test), injecting SomeOtherDependency and IPathWrapper into the constructor
With that done, we’re ready to move on and look at the test itself.
Hold up a minute, what’s this weird mockPathWrapper.Object stuff on line 16?
Here’s the lowdown on Mock.Object: It’s all about type compatibility. Note that mockPathWrapper is declared on line 6 as Mock<IPathWrapper>. The ReallyHardToTest constructor expects us to inject an IPathWrapper, so we can’t directly pass mockPathWrapper. You’ll notice that Mock<T> is generic, which is what allows us to specify the type when creating a mock. Internally, Moq’s code probably looks something like this:
Notice that the Object property is specified as type T (the type we specified when creating the mock). This means that it matches the correct type for the ReallyHardToTest constructor.
Taking a look at the test:
So now let’s take a look at the test method we wrote – there are some interesting things going on here…
This test follows the Arrange, Act, Assert (AAA) pattern, a common unit testing flow.
First, notice that we’re using our old friend TestCaseAttribute so that we are testing two different sets of input values. As we know from before, this means that the test will execute once for each set of values.
Step 1: Arrange
More interesting is line 23, where we configure our mockPathWrapper’s expectations. The line of code is fairly dense, but at a high level, it says: “Hey mockPathWrapper! You should expect me to invoke your GetFileName method with this specific value, and you should return this other value when that happens.”
Now let’s dissect that dense configuration line:
- mockPathWrapper.Setup(…): Setup() (and friends SetupGet/SetupSet for properties) is how we configure expectations on mocks.
- .Setup(p=>p.GetFileName(…)): When we invoke Setup(), we pass an expression telling Moq which method/property we are configuring. Since we’re configuring a method that takes an argument, we need to tell Setup what value will be passed when the method is invoked.
- .Returns(…): Since GetFileName returns a value, we have to tell Moq what value to return when GetFileName is invoked.
Since there is no other configuration required, we are now finished with the “Arrange” phase of the test.
Side Note: We configure the mocks in the test itself because:
- Expected values may change for different test runs, as in our test
- Different tests may involve different mocks, we don’t want unneeded setups because they will cause test failures
Step 2: Act
This one is pretty straightforward – we invoke ReallyHardToTest’s SetFilename method, passing our test value as an argument.
Step 3: Assert
Another straightforward one, we just ask our mock to verify that all of its configured expectations have been met. If so, the test passes, otherwise it fails.
Note: As written, this test will pass. To see it fail, comment out line 14 in the updated ReallyHardToTest class. The test will fail because we are no longer invoking IPathWrapper.GetFileName(…), so the mock’s expectations will not be met.
Congratulations, we’ve just taken a major step towards improving ReallyHardToTest’s design!
Conclusion and Next Steps:
Today we looked at one way to refactor our code to improve testability, and identified one way to work around testability issues with static dependencies:
- Extract an interface matching the methods you use on the static class
- Implement a wrapper for the static class
- Update the constructor to inject the wrapper interface
We also took a super-quick look at how Moq works, covering the topics of expectations, configuration and verification – We’ll dive deeper into these topics in an upcoming tutorial.
Next up, we’ll take a look at how to refactor SomeOtherDependency so it can be mocked!