The target audience for these posts will be GIS focused professionals who work with ArcObjects, who aren't exactly experts in using Unit Testing, Mocking, or Inversion of Control frameworks, but are interested in learning about ways to decouple business logic from dependencies, and improve test-ability generally. For all the software engineering gurus out there, you probably won't learn anything that you don't already know about.
As I mentioned, I intend to release a number of posts over the next couple of weeks that address testing topics, but for this initial post I will only break the ice with a typical trivial contrived example, but rest assured I will be discussing specific ArcObjects testing in the coming weeks. :)
Do Me A SOLID?
A nice introduction to the structure of the applications I will be demonstrating in this series of articles is to reflect on the SOLID principles of object-oriented software design. These principles were described by Uncle Bob Martin quite a few years ago. The letters SOLID stand for -
Single Responsibility - Classes should have a single focus that all their operations are aligned to.
Open/Closed - Classes should be open to extension, but closed for modification.
Liskov Substitution - Subclasses should be able to substitute for the classes they derive from.
Interface Segregation - Classes should support multiple small interfaces rather than one large one.
Dependency Inversion - High level classes should not depend on instances of low level dependencies, but instead depend on abstractions of those dependencies.
Generally we can see that the classes in the ArcObjects framework adhere fairly closely to the SOLID principles, and my topic of testing will focus quite a bit on the last principle - Dependency Inversion, because this is what ultimately makes our applications testable.
Decoupling Our Dependencies
The following example illustrates an approach to decoupling your high level classes from their dependencies to make the code more testable.
In our example, we have started a bar called "The Open Door". Since we were a software developer prior to being a bar proprietor, we decide to write our own point of sale system.
In the example below you can see that we have a Checkout class which acquire's its own drink price list within the CalculateCost method. It then looks at each of the order items and looks up the drink price from the price list, and multiplies this by the quantity of drinks in the order item. The total cost then has 10% tax added.
You can see that the test itself is quite fragile because it makes an assumption about the cost of the drinks to arrive at the expected total, so if the price list changes we will have a problem. One solution might be to get the price list inside the test and look-up the current prices to arrive at the expected total, but then what is to stop us from introducing a bug in this code, and is this what we are really testing? i.e. the looking up of the price? Aren't we really focused on out ability to calculate the total and the additional tax? Regardless, the code in this class can be tested but is tightly coupled to its dependency data.
What if we introduce new business logic? Say on a Friday we want to offer half price drinks from 5pm till 6pm, so we alter the logic as follows.
The code we have written now is very difficult to test because we would need to execute the test at the right time to get a particular expected result. So we have introduced more fragility to our tests.
To overcome the issues we have seen in the past two code examples we will expose our price list and date time provider (clock) as interfaces, and then pass in instances of implementations of those interfaces to our Checkout constructor.
As you can see, the code in the CalculateCost method did not vary dramatically, but the main difference is that we are now passing in instances of our dependencies in the constructor. So you may ask, what was so great about making that change? Well now we have decoupled our code from our dependencies such that we are still tightly integrated using the interfaces but loosely coupled to the concrete implementations such that we could supply other instances of dependencies if we wanted to, i.e. for testing.
The example below shows our new tests which supply the price list (drinksMenu) and clock to the Checkout class.
Since we are mocking our dependencies it means that we don't need our entire application stack in place to perform testing on our business logic. The big benefit with this is that we can run our tests at any time and still verify all of our business logic, allowing developers to be aware of having introduced a bug into existing code, and giving them a fast feedback loop.
Something that may look strange is the use of the mocking framework to create instances of classes that implement IPriceList and IClock and the setup of mocked results to be returned from their method calls. I am using a library called Moq to do this, but in reality I could have created my own class definitions that implement IPriceList and IClock and created methods that returned my expected values. The convenience of creating mocked objects is that I don't have to maintain a lot of redundant concrete implementations just for testing, and can build the mocked expectations on the fly in the tests that they apply to, for example, if we created our own implementations of IClock for happy hour and normal hours, I would need two separate classes, but using the Moq framework I simply define the expected output from the GetDateTimeNow method to be either 6:26pm on Sunday 30th of the June 2013, or 5:13pm on Friday 28th of the June 2013 to suit what I am testing.
We will be looking at the Moq framework in a bit more depth in the coming weeks when we start talking about mocking ArcObjects interfaces, for now just picture a mocking framework as something that allows us to create fake dependency objects on the fly that implement a particular interface and produce results that we specify.
Inversion of Control (IoC)
In the examples shown above we resolved the fragility (from a testing perspective) of our Checkout class by re-factoring our class to be handed instances of its dependencies in the constructor. This structure of development represents the concept of Inversion of Control, where the class doesn't create it's own instances of it's dependencies, but the code calling it does. This form of Inversion of Control is referred to as Dependency Injection, specifically as Constructor based Dependency Injection. If you have heard of Inversion of Control previously it is most likely to have been associated with articles by Martin Fowler, who describes different forms of IoC via Dependency Injection, Factories and Service Locators.
At this point you may be getting worried about -
- What happens if my class required hundreds of dependencies?
- Is this pattern opposed to the concept of encapsulation in object-oriented programming - how will I know that the classes I am using need dependencies supplied, and what their types will be?
In response to the first item, if we build our application using the SOLID principles then our classes should only perform narrow bands of operations that they are responsible for, and should be built up from classes that compose their logic using other dependencies.
In response to the second question, we will not be manually creating instances of dependencies in our own code, we will use an IoC framework to do that for us, which should provide the encapsulation we are familiar with.
Conclusion
Hopefully the examples above gave you a sneak peak into the world of unit testing with mocked objects, and whet your appetite for further discussion on the topics of mocking ArcObjects interfaces. The next topic I will delve into is IoC frameworks, and how we set these up to work with ArcObjects based applications.
No comments:
Post a Comment
Note: Only a member of this blog may post a comment.