Create a Moles Test
Let's see some code for testing. Create a new C# Class project. Paste the following code in the Class1.cs file. This is the class and method we want to unit test. The method copies all files from SourceDirectory, to both DestinationDirectory and BackupDirectory. The method checks that each directory exists, and then creates missing directories. Class1 should require no explanation.using System.IO; namespace InstanceMoleDemo { public class Class1 { public void CopyDirectoryWithBackup(DirectoryInfo source, DirectoryInfo destination, DirectoryInfo backup) { if (!source.Exists) source.Create(); if (!destination.Exists) destination.Create(); if (!backup.Exists) backup.Create(); // TODO - Copy directory files from source to destination and // backup directories... } } }
Now, we need to create a test project. Follow these steps:
- Add a new test project to the solution
- Build the solution
- In the test project, add a reference to the C# Class project (InstanceMoleDemo, in the code above)
- Right-click the test project's reference node -- the Context menu appears
- In the context menu, select Add Moles assembly for mscorlib
- Build the solution -- you'll see more references added for Moles support
using System.IO; using System.IO.Moles; using InstanceMoleDemo; using Microsoft.Moles.Framework; using Microsoft.VisualStudio.TestTools.UnitTesting; [assembly: MoledType(typeof(System.IO.DirectoryInfo))] namespace TestProject1 { [TestClass] public class UnitTest1 { [TestMethod] [HostType("Moles")] public void CopyFileWithBackupTestsBackupDirectory() { // Arrange. var result = false; var c = new Class1(); MDirectoryInfo.AllInstances.ExistsGet = dirInfo => true; MDirectoryInfo.AllInstances.Create = dirInfo => { }; var source = new DirectoryInfo("C:\\Temp\\SourceDirectory"); var destination = new DirectoryInfo("C:\\Temp\\DectinationDirectory"); var backup = new DirectoryInfo("C:\\Temp\\BackupDirectory"); var moledBackup = new MDirectoryInfo(backup); moledBackup.ExistsGet = () => { result = true; return false; }; // Act. c.CopyDirectoryWithBackup(source, destination, backup); // Assert. Assert.IsTrue(result); } } }
This test checks to be sure the method correctly detects the backup directory is missing. To do so, the test must:
- Must not access the file system -- the file system is a dependency that must be isolated
- Indicate the source and destination directories exist
- Indicate the backup directory does not exist
- Prevent any creation of directories on the file system
Examining the Test
The unit test should be ready to run, and fully functional. Please be aware that the test must be executed using the Visual Studio testing tools, as it is not geared for other test harnesses. (See my Moles requires tests to be IN instrumented process post.) Let's step through one piece at a time.using System.IO;
The test is using the System.IO.DirectoryInfo type. This is here for accessibility.using System.IO.Moles;
We want to mole the System.IO.DirectoryInfo type, to detour calls to the Exists and Create methods. The Moles Framework creates the Moles sub-namespace, which we must use.using InstanceMoleDemo;
Of course, we must reference the InstanceMoleDemo assembly, to test it.using Microsoft.Moles.Framework;
This assembly contains the Moles framework, and is required whenever using Moles.using Microsoft.VisualStudio.TestTools.UnitTesting;
Required for unit testing.[assembly: MoledType(typeof(System.IO.DirectoryInfo))]
This assembly attribute streamlines Moles by identifying specific moled types to use, rather than attempting to load all moled types. The mscorlib assembly is quite extensive, and Moles requires substantial overhead. This attribute indicates we are interested only in moling the System.IO.DirectoryInfo type.
[HostType("Moles")]The HostType attribute indicates the test method is dependent on an external host. In this case, the Moles framework is the host, as it will inject detours into the mscorlib assembly, upon compilation. You may have also seen the HostType attribute used with ASP.NET testing.
MDirectoryInfo.AllInstances.ExistsGet = dirInfo => true;There are several things happening on this line. The cumulative effect is that all calls to DirectoryInfo.Exists will always return a true value. This line of code literally tells moles to inject a call to a Func that points to an anonymous method, created through a lambda expression. (This should be nothing new to users of lambda expressions.)
- MDirectoryInfo is the moled DirectoryInfo type
- AllInstances indicates we want to alter all instances of the type
- ExistsGet is the accessor for the Exists property's Get method
- = means we are delegating the call to Exists(get) to the specified method -- in this case, an anonymous method via lambda expressions
- dirInfo is the lambda expression input object
- => is the lambda "goes to" expression
- true is the value to be returned by the anonymous method
MDirectoryInfo.AllInstances.Create = dirInfo => { };This line is very muck like the previous. We are detouring the call to DirectoryInfo.Create to an anonymous method. This method happens to be empty -- we don't want anything to happen when Create is called.
var moledBackup = new MDirectoryInfo(backup);This line of code is the key to this post. We have successfully moled all instances of DirectoryInfo to return always return a true value when Exists is called. However, in the case of the backup instance, we want to return false.
Here, we are instantiating a mole object, MDirectoryInfo, from the existing backup DirectoryInfo object. This is possible, because the mole types inherit the original type. Therefore, passing the backup object as an input value to the MDirectoryInfo constructor returns a mole instance of the backup object.
Once we have a mole instance of the backup object, we can alter the single, mole instance.
moledBackup.ExistsGet = () => { result = true; return false; };We are once again creating a detour for the DirectoryInfo.Exists property. However, there are a couple of differences.
- Because we are dealing with a specific instance, the Exists property does not accept any input parameter. To pass no input parameters into a lambda expression anonymous method, use ().
- We want to execute multiple lines of code in the anonymous method. Therefore, curly braces are required.
- We set result = true, to indicate the test is successful. The result value is the test sentinel value.
Summary
Upon executing the test method, we have asked the Moles framework to do the following tasks:- Always return a true value whenever DirectoryInfo.Exists is called, in any instance of the object
- Never do anything when the DirectoryInfo.Create() method is called.
- When the backup instance of DirectoryInfo.Exists is called, always return false.
If your intention is to test interaction with a dependency (file system, database, network, etc.), you're actually writing an integration test. Integration tests should be separate from unit tests, and executed only after all unit tests are successful. Also, you don't simply write an integration tests to see whether a database server returns correct information -- you have to first test everything in between (network connectivity, appropriate permission and access to connect to the database server, connection to the database server, accessing the database, etc.).
Why can't we mole FileSystemWatcher or WebClient?
ReplyDeleteI also have had the need to detour FileSystemWatcher, myself, but have not yet encountered WebClient. I usspect the reason why these are not moled is that these types interact with operating system handles that are considered unsafe. Detouring portions of the type could negatively impact or compromise the operating system, not to mention present a possible security issue.
ReplyDeleteWhile poking around FileSystemWatcher.StartRaisingEvents, I noted that it calls ThreadPool.BindHandle, which contains the following line, which looks a little ominous:
return ThreadPool.BindIOCompletionCallbackNative(osHandle.DangerousGetHandle());
SOLUTION:
Use constructor dependency injection, to circumvent this problem, as outlined in my post, How to Stub Dependency Event Handlers in Integration Tests