Some Thoughts on Constructors and Unit Testing
This article mainly applies to those languages that have some xUnit-like testing library. I will write the examples in C#, but they should port easily to other languages.
Executive Summary (Yes, that means you.)
- Do not create convenience constructors.
- Use later-than-setup-time creation of objects in unit tests.
Constructors are important
Back in the late 90s during the Rise of Java, I would see APIs like this.
1 | public class AwesomeClass { |
I love me some combinatorial explosions. BOOM!
Ok, I don’t like that. When I want an instance of an object, the constructor communicates to me the expectations that the class has. If a no-argument constructor exists, then that tells me the class can take care of itself. The proliferation of every available combination of constructor parameters as seen above reduces the clarity with which the class’ design can communicate its requirements. And, those requirements should affect the behavior of the class. Otherwise, why have constructor arguments at all?
Constructors in testable/injectable code
When we start using “advanced” programming techniques like dependency injection, the constructor becomes the documentation for the components with which the object fulfills its destined purpose. When we rely on dependency injection containers to construct those objects for us, we have no reason to create the “convenience” constructors found above. We rely on a machine to do the construction.
1 | public class AwesomerClass { |
From that class definition, I know that the AwesomerClass
requires two
arguments to behave in a way appropriate to the system. I know that without
an instance of IReflect
and ISummarize
that I should no have the ability
to create an instance of AwesomerClass
. It should not make sense.
Testing those DI constructors
The wonder and pain of working with dependency injection means changing the signature of the constructor when new dependencies emerge. For example, let’s say I need a class that sends messages across different transports such as email, Twitter, and faxing. (Yes, Virginia, people still fax.) I start off by writing a test for it.
1 | [ ] |
Because, after all, a syndicator can’t work if it has no transport over which to send its message.
Then I write write the code to make the test pass.
1 | public class Syndicator { |
That’s good. So, now I write a test that ensures that the collection of transports is not empty and the code to make it pass.
1 | [ ] |
1 | public class Syndicator { |
And all is well and right in the world. I write 20 other tests exercising the
success and expected failures of the Syndicator
class. At the end of it, I
have a test class that contains the following.
1 | [ ] |
Other than the two argument tests at the top of the fixture, the other tests
use this.transporter
to ensure the proper behavior occurs within
syndicator
. All is well and good…
…Until the time in the project where we have to add logging to the
Syndicator
class so that we can troubleshoot errors in the production
environment. Darn it! I think to myself. Why didn’t I include that in the
first place? Regardless, I have to do it.
Let’s count the number of places that we have to write code to improve this situation.
- Both existing constructor tests will now need to include a logging provider that does not trip any exceptions.
- A new test ensuring that the logging provider parameter does not contain
null
. - A new logging provider parameter in the fields.
- A new mock creation in the setup.
- Passing the new value to the constructor.
Also, we may have to hunt down other places in the test code that creates
instances of Sydnicator
. I’ve had to do this more than once because of new
stories coming in, new dependencies that my class requires.
My testing solution
Whenever I create a new test file for a class that has constructor arguments, I use the following template.
1 | [ ] |
Then, when I come across a test that adds a new constructor parameter, I add a
private field, set it to a reasonable default in the set-up method, set it to
an unreasonable value in my constructor test, and use the CreateMyClass
method as the TestDelegate
. For example, if I were writing that first
Syndicator
test, it would look like this.
1 | [ ] |
I think that this serves my testing purposes much better. I have a
ready-to-use Syndicator
at any time due to the call in RunBeforeEachTest
and I have a centralized TestDelegate
in which my only call to new
occurs.
After ten years of unit testing, this reoccuring form has emerged as a satisfying and easy-to-maintain organization of test code. I hope that it can help your testing efforts, as well.