Design for Unit Testing
Production code must be designed from the start to be unit testable. Trying to retro-fit unit tests after a software component is complete will mean its highly unlikely you can focus your unit tests on one method of code and not invoke other production components.
Walk Through Example
The main goal of unit testing is to isolate the subject under test from all other dependencies. Consider the following code where the FormController is the intended subject under test.
Ideally we need to replace all external dependencies with mocks or stubs. Thereby controlling the input of the class we can predicate the output.
The constructor takes the IContactDataService interface as input argument. Because this interface is essentially an input for this class we need to replace the real implementation with a fake one that the test can control. We can create a new implementation of this interface that can be used instead of the real production implementation. If this was not an interface we would be unable to replace this dependency.
Imagine this constructor argument was not an interface and was a normal class type. This would probably mean we would be using a class that will try to communicate with some external data service, given its name is IContactDataService. For this test to work we would need a fully operational service; and if the service failed our test would fail. This is unacceptable; we do not want our test to fail unless the actual code directly inside the FormController class fails. Fortunately this constructor argument is an interface, therefore it can be replaced.
Now look at the MessageBox property. This property has a return type of IMessageBoxService. At some point the class is going to access this property, probably to interact with a message box dialog. If this property was designed to call the message box functionality in WPF directly this would mean an OS modal message box could pop up at anytime and stall the test until somebody clicks “Ok”. Fortunately we can again provide our own test implementation of this interface and check that the class is invoking the appropriate message box method and passing in the correct data.
The FormController class has three commands it uses to expose functions to the Xaml view. Generally speaking I would not bother mocking an ICommand, seeing as it is an extremely common mechanism and there is little benefit in doing so. If a custom ICommand implementation has been created (other than RoutedCommands and RelayCommands) then mocking it would be recommended.
Lastly we have a ContactDto property. Because DTO’s are basically structs I would not bother mocking this out. This class is simply storing values.
Effectively we can inject our own input into this class and remove its dependencies on all external objects. When we write tests against FormController Class only code failing inside it can fail the test.
The main goal of Unit Testing is to isolate the subject under test from all other dependencies. This is to prevent failing dependencies from failing a test that is only trying to see if the subject under test is broken, not its neighbours.
As a rule, enums, scalar types, and consts need not be interfaced.
Consider another example, imagine a Contact class with properties for Surname and FirstName. During instantiation the class tries to talk to the database to get a requested contact based on constructor arguments. If the database is offline or not configured for testing then the test will fail, and it is not the Contact class’ fault as such. This could be tested, but only by providing a real test database. In this style of testing there are many moving parts that could fail the test that are outside the immediate subject under test. Imagine testing the Delete method on such a class, data will probably be actually erased from a real database.
To prevent situations like this, dependencies must be “interfaced”. This means that all outside dependencies the class needs must be typed to an interface not a concrete type. If database access is done throw an interface, a mock in-memory database can be used instead of a real database, allowing the delete method to be tested, with repeatable consistent results.
Using Inversion of Control (IoC) aka Object Factories
Extensive use of interfaces will require some piece of code somewhere that can tell the application which concrete type to use at runtime. This can be done in many ways but most common is the use of an object factory, also known as an Inversion of Control container or IoC container for short. An object factory is essentially a register of what concrete type to use for which interface.
The benefit an object factory gives you is the ability to configure the factory and change its configuration to “test configuration”. Instead of then manually setting properties using Private Accessors or constructor arguments the class will get its dependencies from the factory. Of course when the application runs in production mode it will be configured to return the real concrete implementations.
Examples of IoC frameworks are Structure Map, Unity, Windsor, Ninject.
Commonly, if you do not make effective use of an object factory you will find your classes have many constructors accepting many different kinds of interface or one constructor with a great deal of arguments.
Object Factory Example
Using Structure Map, getting an instance of an interface is an easy as this:
this.backingMessageBoxService = StructureMap.ObjectFactory.Container.GetInstance<IMessageBoxService>(); |
As stated previously an object factory is essentially a register mapping interfaces to classes. So to make the above code work, you first need to register the interface IMessageBoxService with the object factory. This can be done in one of two ways; either by code, or from the app.config.
By Code:
ObjectFactory.Initialize(init => { init.For<ILogWriter>().Use<FakeLogWriter>(); });
|
Or by App.Config:
<?xml version="1.0" encoding="utf-8" ?> <configuration> <configSections> <section name="StructureMap" type="StructureMap.Configuration.StructureMapConfigurationSection,StructureMap"/> </configSections>
<StructureMap> <DefaultInstance PluginType="MyAssembly.Namespace.IMessageBoxService,MyAssemblyFileName" PluggedType="MyAssembly.Namespace.WpfMessageBox,MyAssemblyFileName" />
…
|
No comments:
Post a Comment