I recently finished reading The Art of Unit Testing, 2nd Edition by Roy Osherove (Amazon). I read the first edition many years ago when I was at a different point in my career (and for interesting reasons that I won't go into here). While going through the book this time, I was comparing Osherove's techniques to the techniques that I've been using. Some of them are the same, and some of them differ a bit. Where things differ, I'm re-evaluating my processes to determine if a different approach may work better for me (and this is one reason I've been exploring NUnit further).
One difference I came across was in the implementation of Property Injection.
Simple Property Injection
The idea behind property injection is that we have a property that will have the production behavior by default. Then for testing, we can swap this out with a fake object by using the property setter.
Osherove shows a simple implementation of this pattern that uses the default getter and setter (like we get when we use the "propfull" code snippet). Then the default value for the property is set in the constructor.
Here's some code that shows this:
This is code that's been modified a bit from my HouseControl application (available on GitHub) to show Osherove's technique.
The object we want to swap out is the "ICommander" property. This is the object responsible for interacting with the hardware in this system.
And what we have is pretty simple:
- A property with a backing field
- Getters and setters that have no logic other than to get or set the value of the backing field
- A constructor where we set the property to our production object (the "SerialCommander")
Swapping in a Fake
When we're testing this object, we can put in a fake "ICommander" by simply setting the property before interacting with the object.
Now any calls to the "Commander" property will be through our fake. And this is a pretty simple class in this case:
This just outputs a message to the console if we're in debug mode. Otherwise it does nothing.
Restrictions on Simple Property Injection
This code actually causes a problem in this application (which I'll explain in a bit). But before I get to that, I want to point out that Osherove mentions the restriction that we've run into:
"When you should use property injectionIn our case, we do meet the criteria of having a dependency where we want a default instance (we want to use the "SerialCommander" when we run the application). But notice the last part: "that doesn't create any problems during the test." This is where our particular application falls down.
Use this technique when you want to signify that a dependency of the class under test is optional or if the dependency has a default instance created that doesn't create any problems during the test." [emphasis mine]
The Art of Unit Testing, p. 63
Hardware Interaction
The whole reason that I need to use property injection is because I wanted to option to test run the application even when I don't have the hardware available (see Rewriting a Legacy App Part 4: Completing the MVP with Scheduling).
Here's the constructor for our "SerialCommander" class:
This tries to initialize a serial port on COM3. The problem is that if we don't have our hardware plugged in to this machine, COM3 doesn't exist, so this code throws an exception.
Here's what happens when we run our test code:
- The "HouseController" constructor creates an instance of the "SerialController".
- The "SerialController" constructor throws an exception since the hardware is not present.
- We never get the chance to set the "Commander" property to our fake object since an exception is thrown.
Safer Property Injection
But all is not lost. We just need to come up with a different way to implement property injection. For this, we change the getter of our property:
Rather than creating the "SerialCommander" in the constructor, we create it in the getter of the property. But we only create it if the backing field is null.
Let's see why this is safer.
Production Run (with Default Object)
When our application runs in production, we want to use the "SerialCommander" object. Here's the process:
- When we create the class, the "commander" backing field is null.
- The first time we try to use the "Commander" property, the backing field is null, so a new "SerialCommander" is created.
- All subsequent calls to the "Commander" property will use the already-instantiated object in the backing field.
Test Run (with Fake Object)
For our test run, we'll use the same code that we saw above:
Here's how that process goes:
- When we create the class, the "commander" backing field is null.
- Before making any calls on the class, we set the "Commander" property to our fake object ("FakeCommander").
- The first time we use the "Commander" property, the backing field is *not* null, so it uses the fake object that has been assigned.
- Any subsequent calls to the "Commander" property will use the fake object in the backing field.
Options Are Good
It's always good to have options. Then we can weigh the pros and cons and pick the right implementation for our application.
In this case, the simple property injection has the advantage of being very easy to implement (we could even use an automatic property if we wanted), but we may run into problems if the default implementation has some requirements we can't meet.
The safer property injection has more complex code, but we can use it in more situations. This safer implementation is how I was introduced to property injection, so this is what I've been using in my code. This has been necessary because I use this to create fake services in my tests. The simple implementation would not work for these scenarios.
For more information on how I've implemented property injection, refer to "Dependency Injection: The Property Injection Pattern" and "Property Injection - Additional Unit Tests".
Wrap Up
So this is one case where I've found my testing techniques to be a little different from Osherove's. I kind of wish that he had shown the more complex implementation as an option, but I'm really glad that he gives the restrictions when he talks about where to use the simple implementation.
Looking at other developers' techniques lets us think about situations a bit differently. Sometimes we see better ways of doing things. Sometimes we see that we're right in line with someone else. And sometimes we find that our current way of doing things has fewer limitations.
By sharing we all get better.
Happy Coding!
Interesting, not yet in full comprehension, but you are a great teacher and it is starting to make more sense. ~Lonnie
ReplyDelete