I’ve talked about Inversion Of Control in previous posts, but I’m going to go over it again. If you’re new to IOC containers, breaking dependencies and unit testing, then this is the blog post you’ll want to read. So let’s get started…
Basic Concept of Unit Testing
Developing and maintaining software is one of the most complex tasks ever performed by humans. Software can grow to proportions that cannot be understood by any one person at a time. To compound the issue of maintaining and enhancing code, there is the problem that one small change in code can affect the operation of something that seems unrelated. Engineers that build something physical, like say a jumbo jet can identify a problem and fix it. They usually don’t expect a problem with the wing to affect the passenger seats. In software, all bets are off. So there needs to be a way to test everything when a small change is made.
The reason you want to create a unit test is to put in place a tiny automatic regression test. This test is executed every time you change code to add an enhancement. If you change some code, the test runs and ensures that you didn’t break a feature that you already coded and tested previously. Each time you add one feature, you add a unit test. Eventually, you end up with a collection of unit tests covering each combination of features used by your software. These tests ride along with your source code forever. Ideally, you want to always regression test every piece of logic that you’ve written. In theory this will prevent you from breaking existing code when you add a new enhancement.
To ensure that you are unit testing properly, you need to understand coverage. Coverage is not everything, but it’s a measurement of how much of your code is covered by your unit tests and you should strive to maximize this. There are tools that can measure this for you, though some are expensive. One aspect of coverage that you need to be aware of is the combination “if” statement:
if (input == 'A' || input =='B') { // do something }
This is a really simple example, but your unit test suite might contain a test that feeds the character A into the input and you’ll get coverage for the inner part of the if statement. However, you have not tested when the input is B and that input might be used by other logic in a slightly different way. Technically, we don’t have 100% coverage. I just want you to be aware that this issue exists and you might need to do some analysis of your code coverage when you’re creating unit tests.
One more thing about unit tests and this is very important to keep in mind. When you deploy this software and bugs are reported, you will need to add a unit test for each bug reported. The unit test must break your code exactly the way the bug did. Then you fix the bug and that prevents any other developer from undoing your bug fix. Of course, your bug fix will be followed by another unit test suite run to make sure you didn’t break any thing else. This will help you make forward progress in your quest for bug-free or low-bug software.
Dependencies
So you’ve learned the basics of unit test writing and you’re creating objects and and putting one or more unit tests on each method. Suddenly you run into an issue. Your object connects to a device for input. An example is that you read from a text file or you connect to a database to read and write data. Your unit test should never cause files to be written or data to be written to a real database. It’s slow, the data being written would need to be cleaned out when the test completed. What if the tests fail? Your test data might still be in the database. Even if you setup a test database, you would not be able to run two versions of your unit tests at the same time (think of two developers executing their local copy of the unit test suite).
The device being used is called a dependency. The object depends on the device and it cannot operate properly without the device. To get around dependencies, we need to create a fake or mock database or a fake file I/O object to put in place of the real database or file I/O when we run our unit tests. The problem is that we need to somehow tell the object under test to use the fake or mock instead of the real thing. The object must also default to the real database or file I/O when not under test.
The current trend in breaking dependencies involves a technique called Inversion Of Control or IOC. What IOC does is allow us to define all object create points at program startup time. When unit tests are run, we substitute the objects that perform database and I/O functions with fakes. Then we call our objects under test and the IOC system takes care of wiring the correct dependencies together. Sounds easy.
IOC Container Basics
Here are the basics of how an IOC container works. I’m going to cut out all the complications involved and keep this super simple.
First, there’s the container. This is a dictionary of interfaces and classes that is used as a lookup. Basically, you create your object and then you create a matching interface for your object. When you call one object from another, you use the interface to lookup which class to call from your object. Here’s a diagram of object A dependent on object B:
Here’s a tiny code sample:
public class A { public void MyMethod() { var b = new B(); b.DependentMethod(); } } public class B { public void DependentMethod() { // do something here } }
As you can see, class B is created inside class A. To break the dependency we need to create an interface for each class and add them to the container:
public interface IB { void DependentMethod(); } public interface IA { void MyMethod(); }
Inside Program.cs:
var serviceProvider = new ServiceCollection() .AddSingleton<IB, B>() .AddSingleton<IA, A>() .BuildServiceProvider(); var a = serviceProvider.GetService<IA>(); a.MyMethod();
Then modify the existing objects to use the interfaces and provide for the injection of B into object A:
public class A : IA { private readonly IB _b; public A(IB b) { _b = b; } public void MyMethod() { _b.DependentMethod(); } } public class B : IB { public void DependentMethod() { // do something here } }
The service collection object is where all the magic occurs. This object is filled with definitions of which interface will be matched with which class. As you can see by the insides of class A, there is no more reference to the class B anywhere. Only the interface is used to reference any object that is passed (called injected) into the constructor that conforms to IB (interface B). The service collection will lookup IB and see that it needs to create an instance of B and pass that along. When the MyMethod() is executed in A, it just calls the _b.DependendMethod() method without worrying about the actual instance of _b. What does that do for us when we are unit testing? Plenty.
Mocking an Object
Now I’m going to use a NuGet package called Moq. This framework is exactly what we need because it can take an interface and create a fake object that we can apply simulated outputs to. First, lets modify our A and B class methods to return some values:
public class B : IB { public int DependentMethod() { return 5; } } public interface IB { int DependentMethod(); } public class A : IA { private readonly IB _b; public A(IB b) { _b = b; } public int MyMethod() { return _b.DependentMethod(); } } public interface IA { int MyMethod(); }
I have purposely kept this so simple that there’s nothing being done. As you can see, DependentMethod() just returns the number 5 in real life. Your methods might perform a calculation and return the result, or you might have a random number generator or it’s a value read from your database. This example just returns 5 and we don’t care about that because our mock object will return any value we want for the unit test being written.
Now the unit test using Moq looks like this:
[Fact] public void ClassATest1() { var mockedB = new Mock<IB>(); mockedB.Setup(b => b.DependentMethod()).Returns(3); var a = new A(mockedB.Object); Assert.Equal(3, a.MyMethod()); }
The first line of the test creates a mock of object B called “mockedB”. The next line creates a fake return for any call to the DependentMethod() method. Next, we create an instance of class A (the real class) and inject the mocked B object into it. We’re not using the container for the unit test because we don’t need to. Technically, we could create a container and put the mocked B object into one of the service collection items, but this is simpler. Keep your unit tests as simple as possible.
Now that there is an instance of class A called “a”, we can assert to test if a.MyMethod() returns 3. If it does, then we know that the mocked object was called by object “a” instead of a real object of class A (since that always returns a 5).
Where to Get the Code
As always you can get the latest code used by this blog post at my GitHub account by clicking here.