These days
I am working slowly on a Windows Phone application. Making my first steps in
this direction turned out to be quite frustrating, although this is a topic for
another post. As a disciplined developer (ha!) I started the project with unit tests
and while writing these I noticed that there is a thing that I do automatically
now. Including a kind of factory method into each and every test class which I
write has become a habit for me, saving a lot of time and effort.
Suppose you
have a class, whose purpose is to get something from somewhere and do a portion
of work on it. Assume further that there is an extensive set of automated tests
for this object available to you. One of the things that may happen in such
circumstances is that you find major flaws in the design of the former
worker-class and decide to change its implementation considerably. At this
point you are going to be happy about the presence of the tests, allowing you
to play with the code and know instantly when you break something. At the same
time there is a little surprise: it turns out that sometimes tests can make the
code that they serve to check not only more reliable, but more inert as well.
You are
most likely to notice this once you chose to introduce a new Inversion of Control
container to simplify the object being redesigned. In this case you are
going to change the class’s constructor and notice suddenly that all your tests
fail even to compile – not to mention passing. Although in many cases
refactoring features built into your favorite IDE can provide some help, the
amount of changes required to make a test-suit at least runnable after adding a
single construction argument might be stunning. In fact it can even make a susceptible developer leave the idea to improve the design. However, there is a
solution helping to avoid this kind of issue.
A good
option for dealing with the problem is making only a tiny portion of the test
class responsible for creating the tested object. This obviously means
introducing a factory method, which would be called from each test-case when an
instance is needed. After this thing is done it is very easy to alter the constructor
arguments, because to make tests compile back you need to change only a couple
of lines of code in one place instead of fixing a dozen scattered through all the test code.
[TestClass] public class MyClassTests { private MyClass CreateInstance() { //Where can we get SomeParameter and AnotherOne? return new MyClass(SomeParameter,AnotherOne); } [TestMethod] public void SomeTest() { var myInstance = CreateInstance(); Assert.IsTrue(myInstance.IsSomething); } //Some more tests - clever or not }
Still, one
may notice that creating a factory method in a test class minimizes future
changes related to altering constructor signature only in case the former is a
zero-argument method itself. This means that one still has to create and tune
the arguments for the constructor elsewhere and find a way to pass these to the
initializer. The latter part of the problem is easily solved by instance-level
variables or properties of the test class:
[TestClass] public class MyClassTests { private SomeType SomeParameter { get; set; } private AnotherType AnotherOne { get; set; } private MyClass CreateInstance() { return new MyClass(SomeParameter,AnotherOne); } [TestMethod] public void SomeTest() { //We initialize parameters - 'context' //The way we need for this particular test SomeParameter = new SomeType(); AnotherOne = new AnotherType(true); //After that we are good to request a properly set up //instance of the tested class var myInstance = CreateInstance(); Assert.IsTrue(myInstance.IsSomething); } //... }
Not bad, but in current setup we still need to write a lot of code in each test method to initialize the properties-arguments to hold desired values. However, I have come to believe that it is almost always possible to find good defaults them – combine this with the test initialization methods available through virtually every unit testing framework and our life will get much easier. We still have to do some tuning specific to particular tests, but this requires writing much less code than otherwise:
[TestClass] public class MyClassTests { private SomeType SomeParameter { get; set; } private AnotherType AnotherOne { get; set; } [TestInitialize] public void Setup() { //Now we set up the context to some reasonable default //before launching each individual test SomeParameter = new SomeType(); AnotherOne = new AnotherType(true); } private MyClass CreateInstance() { return new MyClass(SomeParameter,AnotherOne); } [TestMethod] public void SomeTest() { //The context is ready - no need to do anything var myInstance = CreateInstance(); Assert.IsTrue(myInstance.IsSomething); } [TestMethod] public void SomeTestWithAdditionalTuning() { //Re-initilize a part of context //for the needs of the test AnotherOne = new AnotherType(false); var myInstance = CreateInstance(); Assert.IsFalse(myInstance.IsSomething); } //... }
As you might have guessed, I find this approach particularly useful when one deals with Inversion of Control containers and has to mock them. In this case one doesn’t have to perform all the complex operations required to initialize mocks in every test-case: only setting up the relevant expectations is needed in each individual one, while the default initialization deals with all the rest. This reduces code duplication dramatically and makes the tests less cluttered and more expressive.
[TestClass] public class MyClassTests { private AnotherType AnotherOne { get; set; } private Mock<someType> SomeMock { get; set; } private MockFactory Mocks { get; set; } [TestInitialize] public void Setup() { Mocks = new MockFactory(); SomeParameterMock = Mocks.CreateMock<SomeType>(); AnotherOne = new AnotherType(true); } private MyClass CreateInstance() { return new MyClass(SomeParameterMock.MockObject,AnotherOne); } [TestMethod] public void SomeTest() { //We can set up required expectations //prior to executing factory method SomeMock.Expects(o => o.GetThings()) .WillReturn(new List<Thing>()); var myInstance = CreateInstance(); Assert.IsFalse(myInstance.GotSomeThings); } //... }
A question may arise: why do we abstract away only the construction logic and not the calls to other methods? My very subjective opinion is that the construction is a part of story which is quite different from all the other scenarios of talking to objects – in many test cases the way the object is initialized is simply not relevant at all. When we have mocks and certain expectations related to them, usually only the latter matter, while the calls to something like MockingFramework.CreateMock<YourStuff>(“whatever else”) turning up in tests just distract the reader and hardly bear any value for him. At the same time, as the previous example illustrates, with properties holding construction arguments it is easy to setup expectations in the place where they fit best and are easy to find.
The main
argument against this approach is that moving construction and construction
arguments away from the tests makes the initialization implicit and requires one
to search for it whenever it becomes relevant. More generally, this violates the idea that the
portions of code which are strongly related to each other should reside close to each other as well. Nevertheless, I believe that the benefits are worth this sacrifice and, as I have said above,
construction details might often be of little importance in the context of a particular test.
There is
one more way to benefit from factory methods in tests: they allow us to easily
reuse a portion of tests created for a particular object to test its subclasses.
To do this one has to make the factory method virtual and override it in a
test-class inheriting from existing one:
[TestClass] public class MyBaseClassTests { protected AnotherType AnotherOne { get; set; } protected Mock<SomeType> SomeMock { get; set; } protected MockFactory Mocks { get; set; } [TestInitialize] public void Setup() { Mocks = new MockFactory(); SomeParameterMock = Mocks.CreateMock<SomeType>(); AnotherOne = new AnotherType(true); } protected virtual MyClass CreateInstance() { return new MyClass(SomeParameterMock.MockObject,AnotherOne); } [TestMethod] public void SomeTest() { //We can set up required expectations prior to executing factory method SomeMock.Expects(o => o.GetThings()).WillReturn(new List<Thing>()); var myInstance = CreateInstance(); Assert.IsFalse(myInstance.GotSomeThings); } //... } [TestClass] public class MySubClassTests : MyBaseClassTests { protected override MyClass CreateInstance() { return new MySubClass(SomeParameterMock.MockObject,AnotherOne); } //Even though we didn't write anything else //we do have some tests already! }
This way, have we decided to create a more specific version of an existing class, we can get an initial test-suit for it virtually at no cost. To be honest, most frequently there is cost – adopting existing set of automated tests to a new kind of object requires some investment. This is usually related to the fact that some assumptions about the base class do not necessarily hold with a subclass (which is a clear violation of the substitution principle (LSP), though a common one), some mocked expectations may make no sense in the context of a subclass and so on. Still, in my personal experience having an inherited test suit to start with makes things easier more often than not. Furthermore, in case it doesn’t this may point to problems with architecture – ignoring the LSP almost always suggest there are some. So testing awards us not just with confidence in the correctness of code, but also with deeper understanding of the chosen design and its strong and weak sides.
In
conclusion I must pay some attention to the name I use to describe this
approach. In our age of design patterns there is hardly
anybody who doesn’t know that Factory Method is a construction pattern. Its traditional form as well as purpose may look different from what
I focus on here, although in both cases factory method serves as an entry point
to a particular class – a way to get hold of its instance. The only difference is
the context: the factory method in a test-suite narrows the scope and
encapsulates the logic relevant to setting up an object with a goal of passing
a particular list of tests. On the other side, the common factory method is a
gateway to creating instances of the desired type for any other consumer in the system,
including the test classes, so in many situations it is absolutely valid to
call a factory method in a factory method.
The idea
that one can benefit from design patterns not only in the application code
itself, but also in tests stands for me among the most enlightening concepts
related to unit testing and TDD. In fact, it once made me understand and reminds me that the tests,
which we write, are not just a nice tooling or a way to get believing that some
stuff works. Tests are first-class citizens of our products, both driving them and explaining them, and since they take
the form of code we can’t afford ignoring any of the ways to make this code better.
Комментариев нет:
Отправить комментарий