The code you work on has this wonderful simple method. All it does is use a couple of other classes, passing through the parameters it received and setting some properties.
class SimpleClass { public SimpleResult SimplePassThrough(string someString, int someInteger) { SimpleResult result = new SimpleResult(); RealWork work = new RealWork(); if (!work.DoActualWork(someString, someInteger)) { result.Error = work.Error; } return result; } }
As it is such a simple function nobody bothered to write any tests for it. After all, “It only passes through to another class.”
But now that decision has come back to bite you in the a.., because …
Hmm, never mind, the “why” doesn’t really matter anymore. You just happen to be the lucky one to bring this little beauty under test.
As simple as it is, SimplePassThrough
is quite untestable.
You know how to create test doubles and use them. But whoever wrote this function, in his or her unending wisdom, decided to instantiate specific classes right there in SimplePassThrough
itself. Now how are you going to make it use your test doubles to ensure that all paths in SimplePassThrough
are exercised?
Even using a mocking framework there is no way to make this function use your mocks instead of the classes it specifically instantiates!
Grrrr.
Bad news. You are right. You can’t get this function under test without changing it.
Good news is, the changes you need to make are pretty straightforward.
And you don’t need any mocking framework, nor any inversion of control container to do it.
All you need to do is:
- Add factory methods to
SimpleClass
to return instances of the classes thatSimplePassThrough
needs to do its work. - Change
SimplePassThrough
to use these factory methods instead of instantiating the classes itself. - Add a test double for
RealWork
- In your test code, declare a descendant of
SimpleClass
and override the factory methods for the instances that you want to replace with your test double. - In your tests, instantiate this test descendant so the overridden versions of the factory methods will be used.
Adding factory methods
Factory methods are essentially methods that return (new) instances. The Factory Method pattern as discussed by the Gang of Four, is a little more involved using interfaces and a separate Factory class. The essence is the same: returning references to instances without the caller of the Factory (Method) knowing or caring which exact class (or even which instance) is providing the implementation.
For SimpleClass
you need two factory methods. One for SimpleResult
, the other for RealWork
. The implementation in SimpleClass
is straightforward: just return a new instance of the desired class.[1]
The methods still specify exactly which classes SimpleClass
will use. Not ideal, but ok for the goal of making SimplePassThrough
testable.
Both MakeResult
and MakeWork
are marked virtual so you can override them to get SimplePassThrough
to work with instances of your test doubles.
virtual protected SimpleResult MakeResult() { return new SimpleResult(); } virtual protected RealWork MakeWork() { return new RealWork(); }
Changing SimplePassThrough
to use factory methods
With the factory methods in place you can change SimplePassThrough
to work with whatever the factory methods provide. To achieve that, change[2]
SimpleResult result = new SimpleResult(); RealWork work = new RealWork();
to
SimpleResult result = MakeResult(); RealWork work = MakeWork();
Add a test double for RealWork
The RealWork
test double class is where you ensure that all paths in SimplePassThrough
can be exercised by overriding DoActualWork
. If DoActualWork
isn’t virtual yet in the RealWork
class, you’ll have to make it so.[3]
class RealWorkTestDouble : RealWork { override public Boolean DoActualWork(string aString, int aInt) { if (aInt != 100) Error = 550; return aInt == 100; } }
This test double ensures that you can call it with 100
in aInt
to return true
and with any other value to return false
and set Error
to 550
. [4]
The class SimpleResult
doesn’t need a double because it happens to be a class with just some read/write properties such as Error
.
Declaring the SimpleClass
test descendant
There is nothing anywhere that says you can only create descendants that will be used in production code. There also is nothing anywhere that says that you are obliged to instantiate the exact class under test in your tests for a class. In fact using a descendant class of the class under test is often a good way to break dependencies and provide “stubs” for methods called by the method under test.
So derive a class from SimpleClass
, override its MakeWork
method and implement that to return an instance of your RealWork
test double.
class CutDescendant : SimpleClass { override protected RealWork MakeWork() { return new RealWorkTestDouble(); } }
Testing SimplePassThrough
SimplePassThrough
is indeed a pretty simple function, but it still has logic that you want to stop falling over without someone noticing. So you add two tests. One to verify what should happen when RealWork.DoActualWork
returns true
and one for when it returns false.
[TestMethod] public void SimplePassThrough_ActualWork_ReturnsTrue_ShouldReturn_NoError() { // Arrange SimpleClass cut = new CutDescendant(); // Act SimpleResult result = cut.SimplePassThrough("Ok", 100); // Assert Assert.AreEqual(0, result.Error); } [TestMethod] public void SimplePassThrough_ActualWork_ReturnsFalse_ShouldReturn_ErrorFromWork() { // Arrange SimpleClass cut = new CutDescendant(); // Act SimpleResult result = cut.SimplePassThrough("Error", 500); // Assert Assert.AreEqual(550, result.Error); }
That’s it.
Now go out there and write some tests for that method you thought was untestable
Notes
[1] Would be even better to create factory methods that return an interface instead of an object reference. That way your test doubles don’t have to derive from the original classes used and you don’t have to mark any methods in those classes virtual
to provide specific test behavior.
[2] When you forget to change SimplePassThrough
to use the factory methods, at least one of the tests should fail: the one checking for a non-zero Error.
[3] When you forget to make DoActualWork
virtual and use override in the test descendant, at least one of the tests is likely to fail, probably the one checking for a non-zero Error.
[4] When you use different values in your Asserts than the values that control the behavior of your test doubles, then you ensure that the class you are testing really uses the value it should. Had you same value as you pass in, then your test would not detect whether SimplePassThrough
set result.Error
to someInteger
instead of work.Error
.
Leave a Reply