"Dependencies" includes other modules, APIs, frameworks, hardware, databases, networks, etc. We want to ensure the application works with these other resources as expected. This demonstration code implements constructor injection, to simplify testing. Dependency injection is essential to unit testing, but also proves useful for integration testing. The injection is important, because we want to isolate a specific dependency and inject a controlled mock object for others.
Set Up Projects
- Create a new Class Library project named "ClassLibrary1"
- Copy the following code into the Class1.cs library:
using System.IO; namespace ClassLibrary1 { public class Class1 { FileSystemWatcher watcher; public Class1(string directoryPathToWatch) { FileCreated = false; watcher = new FileSystemWatcher(directoryPathToWatch); watcher.Created += new FileSystemEventHandler(watcher_Created); watcher.Filter = "*.txt"; watcher.EnableRaisingEvents = true; } void watcher_Created(object sender, FileSystemEventArgs e) { FileCreated = true; } private bool _fileCreated; public bool FileCreated { get { return _fileCreated; } set { _fileCreated = value; } } } }
- Right-click the watcher_Created method name -- the context menu appears
- Select the Create Unit Tests... option in the context menu
- When prompted, create a test project named TestProject1.
Create Integration Test
We need to test the FileSystemWatcher object, to ensure it is correctly calling the watcher_Created event handler. (Sure, this is a .NET Framework object that should be reliable, but this is simply for demonstration purposes.)- Open Class1Test.cs, if it is not already open
- Add using statements for the following namespaces:
using System.IO; using System.Threading;
- Delete everything inside Class1Test
- Create a test method, named WatcherReportsCreationOfTxtFiles:
[TestMethod] public void WatcherReportsCreationOfTxtFile() { // Arrange. // Act. // Assert. Assert.Inconclusive("This test is not complete."); }
- Let's arrange a Class1_Accessor object and some other variables. Because we are watching for file creation events, we want to specify a controlled directory instead of another one that may be in active use:
[TestMethod] public void WatcherReportsCreationOfTxtFile() { // Arrange. Class1_Accessor target = new Class1_Accessor(@"C:\Temp\FileTest"); var path = @"C:\Temp\FileTest\test.txt"; var result = false; // Act. // Assert. Assert.Inconclusive("This test is not complete."); }
- We are watching for file creation events. If we create a file that already exists, our watcher will not see this as a new file being created. Rather, this is considered a file modification. Therefore, it is necessary to delete our target file, for a good test. Don't forget, we must wait for the file to be removed from disk! Add the following lines to the Act section:
File.Delete(path); Thread.Sleep(100); // Allow time to delete the file.
- When we create a new file, the watcher is going to call its handler. We aren't interested in the handler being called. In fact, we should disable it, to prevent executing more code than absolutely necessary. Therefore, we want to unsubscribe the original handler from the delegate:
target.watcher.Created -= target.watcher_Created;
- We need to determine if the Created event is raised. We may do so by creating our own, anonymous method. This method will set the value of the result boolean to true, indicating the call happened. We can use lambda expressions, to make this a one-line solution, rather than creating another method in the test class:
target.watcher.Created += (object sender, FileSystemEventArgs e) => result = true;
- It's time to act! Create the test file, and wait for the file to be written to disk:
// Act. File.Create(path); Thread.Sleep(100); // Allow time to create the file.
- Finally, we must assert the value of result is true. Remove any existing assertion from the method, and then assert:
// Assert. Assert.IsTrue(result);
The complete Class1Test.cs file should look like this:
using System.IO; using System.Threading; using ClassLibrary1; using Microsoft.VisualStudio.TestTools.UnitTesting; namespace TestProject1 { [TestClass] public class Class1Test { [TestMethod] public void WatcherReportsCreationOfTxtFile() { // Arrange. Class1_Accessor target = new Class1_Accessor(@"C:\Temp\FileTest"); var path = @"C:\Temp\FileTest\test.txt"; var result = false; File.Delete(path); Thread.Sleep(100); // Allow time to delete the file. target.watcher.Created -= target.watcher_Created; target.watcher.Created += (object sender, FileSystemEventArgs e) => result = true; // Act. File.Create(path); Thread.Sleep(100); // Allow time to create the file. // Assert. Assert.IsTrue(result); } [TestMethod] public void WatcherDoesNotReportCreationOfCsvFile() { // Arrange. Class1_Accessor target = new Class1_Accessor(@"C:\Temp\FileTest"); var path = @"C:\Temp\FileTest\test.csv"; var result = false; File.Delete(path); Thread.Sleep(100); // Allow time to delete the file. target.watcher.Created -= target.watcher_Created; target.watcher.Created += (object sender, FileSystemEventArgs e) => result = true; // Act. File.Create(path); Thread.Sleep(100); // Allow time to create the file. // Assert. Assert.IsFalse(result); // NOTE WE ARE TESTING TO ENSURE THE HANDLER WAS *NOT* CALLED! } } }
Execute the tests. They should both pass!
Summary
This code accomplished several things:- Tested a specific dependency (the local file system)
- Exposed private members of the target class
- Minimized target code execution
- Subscribed an anonymous method for the sole purpose of validating the test
- Employed a lambda expression, which is always a cool thing to do
No comments:
Post a Comment
Please provide details, when posting technical comments. If you find an error in sample code or have found bad information/misinformation in a post, please e-mail me details, so I can make corrections as quickly as possible.