Introduction
In this post, I’m going to talk about basic dependency injection and mocking a method that is used to access hardware. The method I’ll be mocking is the System.IO.Directory.Exists().
Mocking Methods
One of the biggest headaches with unit testing is that you have to make sure you mock any objects that your method under test is calling. Otherwise your test results could be dependent on something you’re not really testing. As an example for this blog post, I will show how to apply unit tests to this very simple program:
class Program { static void Main(string[] args) { var myObject = new MyClass(); Console.WriteLine(myObject.MyMethod()); Console.ReadKey(); } }
The object that is used above is:
public class MyClass { public int MyMethod() { if (System.IO.DirectoryExists("c:\\temp")) { return 3; } return 5; } }
Now, we want to create two unit tests to cover all the code in the MyMethod() method. Here’s an attempt at one unit test:
[TestMethod] public void test_temp_directory_exists() { var myObject = new MyClass(); Assert.AreEqual(3, myObject.MyMethod()); }
The problem with this unit test is that it will pass if your computer contains the c:\temp directory. If your computer doesn’t contain c:\temp, then it will always fail. If you’re using a continuous integration environment, you can’t control if the directory exists or not. To compound the problem you really need test both possibilities to get full test coverage of your method. Adding a unit test to cover the case where c:\temp to your test suite would guarantee that one test would pass and the other fail.
The newcomer to unit testing might think: “I could just add code to my unit tests to create or delete that directory before the test runs!” Except, that would be a unit test that modifies your machine. The behavior would destroy anything you have in your c:\temp directory if you happen to use that directory for something. Unit tests should not modify anything outside the unit test itself. A unit test should never modify database data. A unit test should not modify files on your system. You should avoid creating physical files if possible, even temp files because temp file usage will make your unit tests slower.
Unfortunately, you can’t just mock System.IO.Directory.Exists(). The way to get around this is to create a wrapper object, then inject the object into MyClass and then you can use Moq to mock your wrapper object to be used for unit testing only. Your program will not change, it will still call MyClass as before. Here’s the wrapper object and an interface to go with it:
public class FileSystem : IFileSystem { public bool DirectoryExists(string directoryName) { return System.IO.Directory.Exists(directoryName); } } public interface IFileSystem { bool DirectoryExists(string directoryName); }
Your next step is to provide an injection point into your existing class (MyClass). You can do this by creating two constructors, the default constructor that initializes this object for use by your method and a constructor that expects a parameter of IFileSystem. The constructor with the IFileSystem parameter will only be used by your unit test. That is where you will pass along a mocked version of your filesystem object with known return values. Here are the modifications to the MyClass object:
public class MyClass { private readonly IFileSystem _fileSystem; public MyClass(IFileSystem fileSystem) { _fileSystem = fileSystem; } public MyClass() { _fileSystem = new FileSystem(); } public int MyMethod() { if (_fileSystem.DirectoryExists("c:\\temp")) { return 3; } return 5; } }
This is the point where your program should operate as normal. Notice how I did not need to modify the original call to MyClass that occurred at the “Main()” of the program. The MyClass() object will create a IFileSystem wrapper instance and use that object instead of calling System.IO.Directory.Exists(). The result will be the same. The difference is that now, you can create two unit tests with mocked versions of IFileSystem in order to test both possible outcomes of the existence of “c:\temp”. Here is an example of the two unit tests:
[TestMethod] public void test_temp_directory_exists() { var mockFileSystem = new Mock<IFileSystem>(); mockFileSystem.Setup(x => x.DirectoryExists("c:\\temp")).Returns(true); var myObject = new MyClass(mockFileSystem.Object); Assert.AreEqual(3, myObject.MyMethod()); } [TestMethod] public void test_temp_directory_missing() { var mockFileSystem = new Mock<IFileSystem>(); mockFileSystem.Setup(x => x.DirectoryExists("c:\\temp")).Returns(false); var myObject = new MyClass(mockFileSystem.Object); Assert.AreEqual(5, myObject.MyMethod()); }
Make sure you include the NuGet package for Moq. You’ll notice that in the first unit test, we’re testing MyClass with a mocked up version of a system where “c:\temp” exists. In the second unit test, the mock returns false for the directory exists check.
One thing to note: You must provide a matching input on x.DirectoryExists() in the mock setup. If it doesn’t match what is used in the method, then you will not get the results you expect. In this example, the directory being checked is hard-coded in the method and we know that it is “c:\temp”, so that’s how I mocked it. If there is a parameter that is passed into the method, then you can mock some test value, and pass the same test value into your method to make sure it matches (the actual test parameter doesn’t matter for the unit test, only the results).
Using an IOC Container
This sample is setup to be extremely simple. I’m assuming that you have existing .Net legacy code and you’re attempting to add unit tests to the code. Normally, legacy code is hopelessly un-unit testable. In other words, it’s usually not worth the effort to apply unit tests because of the tightly coupled nature of legacy code. There are situations where legacy code is not too difficult to add unit testing. This can occur if the code is relatively new and the developer(s) took some care in how they built the code. If you are building new code, you can use this same technique from the beginning, but you should also plan your entire project to use an IOC container. I would not recommend refactoring an existing project to use an IOC container. That is a level of madness that I have attempted more than once with many man-hours of wasted time trying to figure out what is wrong with the scoping of my objects.
If your code is relatively new and you have refactored to use contructors as your injection points, you might be able to adapt to an IOC container. If you are building your code from the ground up, you need to use an IOC container. Do it now and save yourself the headache of trying to figure out how to inject objects three levels deep. What am I talking about? Here’s an example of a program that is tightly coupled:
class Program { static void Main(string[] args) { var myRootClass = new MyRootClass(); myRootClass.Increment(); Console.WriteLine(myRootClass.CountExceeded()); Console.ReadKey(); } } public class MyRootClass { readonly ChildClass _childClass = new ChildClass(); public bool CountExceeded() { if (_childClass.TotalNumbers() > 5) { return true; } return false; } public void Increment() { _childClass.IncrementIfTempDirectoryExists(); } } public class ChildClass { private int _myNumber; public int TotalNumbers() { return _myNumber; } public void IncrementIfTempDirectoryExists() { if (System.IO.Directory.Exists("c:\\temp")) { _myNumber++; } } public void Clear() { _myNumber = 0; } }
The example code above is very typical legacy code. The “Main()” calls the first object called “MyRootClass()”, then that object calls a child class that uses System.IO.Directory.Exists(). You can use the previous example to unit test the ChildClass for examples when c:\temp exist and when it doesn’t exist. When you start to unit test MyRootClass, there’s a nasty surprise. How to you inject your directory wrapper into that class? If you have to inject class wrappers and mocked classes of every child class of a class, the constructor of a class could become incredibly large. This is where IOC containers come to the rescue.
As I’ve explained in other blog posts, an IOC container is like a dictionary of your objects. When you create your objects, you must create a matching interface for the object. The index of the IOC dictionary is the interface name that represents your object. Then you only call other objects using the interface as your data type and ask the IOC container for the object that is in the dictionary. I’m going to make up a simple IOC container object just for demonstration purposes. Do not use this for your code, use something like AutoFac for your IOC container. This sample is just to show the concept of how it all works. Here’s the container object:
public class IOCContainer { private static readonly Dictionary<string,object> ClassList = new Dictionary<string, object>(); private static IOCContainer _instance; public static IOCContainer Instance => _instance ?? (_instance = new IOCContainer()); public void AddObject<T>(string interfaceName, T theObject) { ClassList.Add(interfaceName,theObject); } public object GetObject(string interfaceName) { return ClassList[interfaceName]; } public void Clear() { ClassList.Clear(); } }
This object is a singleton object (global object) so that it can be used by any object in your project/solution. Basically it’s a container that holds all pointers to your object instances. This is a very simple example, so I’m going to ignore scoping for now. I’m going to assume that all your objects contain no special dependent initialization code. In a real-world example, you’ll have to analyze what is initialized when your objects are created and determine how to setup the scoping in the IOC container. AutoFac has options of when the object will be created. This example creates all the objects before the program starts to execute. There are many reasons why you might not want to create an object until it’s actually used. Keep that in mind when you are looking at this simple example program.
In order to use the above container, we’ll need to use the same FileSystem object and interface from the prevous program. Then create an interface for MyRootObject and ChildObject. Next, you’ll need to go through your program and find every location where an object is instantiated (look for the “new” command). Replace those instances like this:
public class ChildClass : IChildClass { private int _myNumber; private readonly IFileSystem _fileSystem = (IFileSystem)IOCContainer.Instance.GetObject("IFileSystem"); public int TotalNumbers() { return _myNumber; } public void IncrementIfTempDirectoryExists() { if (_fileSystem.DirectoryExists("c:\\temp")) { _myNumber++; } } public void Clear() { _myNumber = 0; } }
Instead of creating a new instance of FileSystem, you’ll ask the IOC container to give you the instance that was created for the interface called IFileSystem. Notice how there is no injection in this object. AutoFac and other IOC containers have facilities to perform constructor injection automatically. I don’t want to introduce that level of complexity in this example, so for now I’ll just pretend that we need to go to the IOC container object directly for the main program as well as the unit tests. You should be able to see the pattern from this example.
Once all your classes are updated to use the IOC container, you’ll need to change your “Main()” to setup the container. I changed the Main() method like this:
static void Main(string[] args) { ContainerSetup(); var myRootClass = (IMyRootClass)IOCContainer.Instance.GetObject("IMyRootClass"); myRootClass.Increment(); Console.WriteLine(myRootClass.CountExceeded()); Console.ReadKey(); } private static void ContainerSetup() { IOCContainer.Instance.AddObject<IChildClass>("IChildClass",new ChildClass()); IOCContainer.Instance.AddObject<IMyRootClass>("IMyRootClass",new MyRootClass()); IOCContainer.Instance.AddObject<IFileSystem>("IFileSystem", new FileSystem()); }
Technically the MyRootClass object does not need to be included in the IOC container since no other object is dependent on it. I included it to demonstrate that all objects should be inserted into the IOC container and referenced from the instance in the container. This is the design pattern used by IOC containers. Now we can write the following unit tests:
[TestMethod] public void test_temp_directory_exists() { var mockFileSystem = new Mock<IFileSystem>(); mockFileSystem.Setup(x => x.DirectoryExists("c:\\temp")).Returns(true); IOCContainer.Instance.Clear(); IOCContainer.Instance.AddObject("IFileSystem", mockFileSystem.Object); var myObject = new ChildClass(); myObject.IncrementIfTempDirectoryExists(); Assert.AreEqual(1, myObject.TotalNumbers()); } [TestMethod] public void test_temp_directory_missing() { var mockFileSystem = new Mock<IFileSystem>(); mockFileSystem.Setup(x => x.DirectoryExists("c:\\temp")).Returns(false); IOCContainer.Instance.Clear(); IOCContainer.Instance.AddObject("IFileSystem", mockFileSystem.Object); var myObject = new ChildClass(); myObject.IncrementIfTempDirectoryExists(); Assert.AreEqual(0, myObject.TotalNumbers()); } [TestMethod] public void test_root_count_exceeded_true() { var mockChildClass = new Mock<IChildClass>(); mockChildClass.Setup(x => x.TotalNumbers()).Returns(12); IOCContainer.Instance.Clear(); IOCContainer.Instance.AddObject("IChildClass", mockChildClass.Object); var myObject = new MyRootClass(); myObject.Increment(); Assert.AreEqual(true,myObject.CountExceeded()); } [TestMethod] public void test_root_count_exceeded_false() { var mockChildClass = new Mock<IChildClass>(); mockChildClass.Setup(x => x.TotalNumbers()).Returns(1); IOCContainer.Instance.Clear(); IOCContainer.Instance.AddObject("IChildClass", mockChildClass.Object); var myObject = new MyRootClass(); myObject.Increment(); Assert.AreEqual(false, myObject.CountExceeded()); }
In these unit tests, we put the mocked up object used by the object under test into the IOC container. I have provided a “Clear()” method to reset the IOC container for the next test. When you use AutoFac or other IOC containers, you will not need the container object in your unit tests. That’s because IOC containers like the one built into .Net Core and AutoFac use the constructor of the object to perform injection automatically. That makes your unit tests easier because you just use the constructor to inject your mocked up object and test your object. Your program uses the IOC container to magically inject the correct object according to the interface used by your constructor.
Using AutoFac
Take the previous example and create a new constructor for each class and pass the interface as a parameter into the object like this:
private readonly IFileSystem _fileSystem; public ChildClass(IFileSystem fileSystem) { _fileSystem = fileSystem; }
Instead of asking the IOC container for the object that matches the interface IFileSystem, I have only setup the object to expect the fileSystem object to be passed in as a parameter to the class constructor. Make this change for each class in your project. Next, change your main program to include AutoFac (NuGet package) and refactor your IOC container setup to look like this:
static void Main(string[] args) { IOCContainer.Setup(); using (var myLifetime = IOCContainer.Container.BeginLifetimeScope()) { var myRootClass = myLifetime.Resolve<IMyRootClass>(); myRootClass.Increment(); Console.WriteLine(myRootClass.CountExceeded()); Console.ReadKey(); } } public static class IOCContainer { public static IContainer Container { get; set; } public static void Setup() { var builder = new ContainerBuilder(); builder.Register(x => new FileSystem()) .As<IFileSystem>() .PropertiesAutowired() .SingleInstance(); builder.Register(x => new ChildClass(x.Resolve<IFileSystem>())) .As<IChildClass>() .PropertiesAutowired() .SingleInstance(); builder.Register(x => new MyRootClass(x.Resolve<IChildClass>())) .As<IMyRootClass>() .PropertiesAutowired() .SingleInstance(); Container = builder.Build(); } }
I have ordered the builder.Register command from innner most to the outer most object classes. This is not really necessary since the resolve will not occur until the IOC container is called by the object to be used. In other words, you can define the MyRootClass first, followed by FileSystem and ChildClass, or in any order you want. The Register command is just storing your definition of which physical object will be represented by each interface and which dependencies it will depend on.
Now you can cleanup your unit tests to look like this:
[TestMethod] public void test_temp_directory_exists() { var mockFileSystem = new Mock<IFileSystem>(); mockFileSystem.Setup(x => x.DirectoryExists("c:\\temp")).Returns(true); var myObject = new ChildClass(mockFileSystem.Object); myObject.IncrementIfTempDirectoryExists(); Assert.AreEqual(1, myObject.TotalNumbers()); } [TestMethod] public void test_temp_directory_missing() { var mockFileSystem = new Mock<IFileSystem>(); mockFileSystem.Setup(x => x.DirectoryExists("c:\\temp")).Returns(false); var myObject = new ChildClass(mockFileSystem.Object); myObject.IncrementIfTempDirectoryExists(); Assert.AreEqual(0, myObject.TotalNumbers()); } [TestMethod] public void test_root_count_exceeded_true() { var mockChildClass = new Mock<IChildClass>(); mockChildClass.Setup(x => x.TotalNumbers()).Returns(12); var myObject = new MyRootClass(mockChildClass.Object); myObject.Increment(); Assert.AreEqual(true, myObject.CountExceeded()); } [TestMethod] public void test_root_count_exceeded_false() { var mockChildClass = new Mock<IChildClass>(); mockChildClass.Setup(x => x.TotalNumbers()).Returns(1); var myObject = new MyRootClass(mockChildClass.Object); myObject.Increment(); Assert.AreEqual(false, myObject.CountExceeded()); }
Do not include the AutoFac NuGet package in your unit test project. It’s not needed. Each object is isolated from all other objects. You will still need to mock any injected objects, but the injection occurs at the constructor of each object. All dependencies have been isolated so you can unit test with ease.
Where to Get the Code
As always, I have posted the sample code up on my GitHub account. This project contains four different sample projects. I would encourage you to download each sample and experiment/practice with them. You can download the samples by following the links listed here: