Programmers who have incorporated unit testing into their development process already know its advantages: cleaner code, confidence to refactor, and higher speed of development. But even the most die-hard unit testers can falter when faced with testing a class that relies on system state for its behavior. This article looks at the isolation pattern that can help you solve these problems.
Many developers have seen the improvement in quality and increase in speed that comes from having a comprehensive unit test suite. But writing good unit tests takes time and effort. Because each unit cooperates with other units, writing a unit test can involve a significant amount of setup code. In many cases, they can be almost impossible to implement, especially when methods depend on other hard-to-control things such as the network, a database, or even ambient temperature. The reliance on frameworks and the way their authors have decided to write them can also affect how we write our tests, and if we can even succeed.
In our Previous article, we looked at some unit test patterns and saw that there are many cases in which unit test patterns are not enough. As a result, developers have to refactor their code to make it ‘testable’. Examples of code that needs to change include:
- Singleton classes
- Calls to static members
- Objects that do not implement an interface (as required for the mock object pattern)
- Objects that are instantiated in the code being tested
- Objects that are not passed to the method (as required for mock object pattern)
The main problem is that refactoring without unit tests, to make the code testable, e.g. for the benefit of writing tests for it, does not make sense. It’s risky and costly.
Let’s go back to our example of an Authentication class, as seen in the first part of this series. We will see some code that is impossible to test without modifications.
We have decided to use a Logger in the code. The Logger is responsible for logging the authentication process. This class reads an external configuration (perhaps from the database) and decides what severity to log and where to log. Logs of different severities can be logged in different places and could even send an email to the system manager. The Logger already has vast tests and we don’t need to test these again.
Let’s look at the code again:
Note our code also usesa DataConnection class to setup our database.
How do we test this? Let’s test how our code reacts to a system failure. This can occur, for example, if the user database is corrupt. We can do one of the following:
- Set up the logging system with all its subsystems (for example, the email subsystem) and check that the logs have been sent. This can lead to substantial setup and a greater coupling of the tests to the logging code. If the logger is reconfigured to divert the logs to a file instead of an email, the test will fail, while the authentication logic is correct.
- Modify the logging code to flag the system if we are in testing mode. For example, Logger.SetTestingMode=true. This will lead to having debugged code (basically a test supporting code) in our production code. We also may come up with a different code for testing and production, in which case we would not be really testing our production code.
- We could create an ILogger interface and let the Logger implement it and use a mock object instead. However, for that we’ll need to change both the Logger and our code, going back to the price and risk making code testable.
In addition, we have to make these changes in the DataConnection class as well. What if there are more dependencies like that?
Using Typemock Isolator to Isolate the Dependencies
Typemock Isolator uses “code weaving” to change the behavior of the production code at runtime, without having to change the real code. The object Typemock Isolator creates are called fake objects, that look exactly like the real objects (they have the same type), but we can control how each method behaves at test time.
Let’s see how a unit test looks like using Typemock Isolator for the setup:
public void IsAuthenticated_WhenCriticalError_ReturnFalse()
// Ignore all calls to the Logger Static methods
// Throw an exception when Initialize gets called
.WillThrow(new Exception("Initialization error"));
Authentication authenticator = new Authentication();
bool result = authenticator.IsAuthenticated("user","password");
Let’s see what we did:
|1||Arrange the behaviors up front: Fake all the Logger class’ methods, regardless of arguments|
|2||Also, simulate throwing an exception when DataConnection.Initialize() gets called|
|3||Invoke the IsAuthenticated method, the code we want to test|
|4||Assert the result of the method based on the behavior we arranged.|
The attentive reader may have noticed that we don’t pass any faked object to the code we are testing. We just set up the behavior we want to run at test time. This code is much more elegant than the other solutions, and our test is written according to unit testing best practices:
- Test isolated code.
We isolate our code completely from its dependencies by returning faked values.
- Tests should be fast.
We don’t need to go through database initialization, which makes the test faster.
- Tests have to be self-contained.
The entire scenario is contained in the test, no database is needed since we simulate its behavior. The database is not touched. This test can be run alone or as part of a suite and will give the correct result every time.
- Tests have to be run by multiple users.
Since the test is self-contained and the code is isolated, this test can be run anywhere, anytime.
The test is short, readable, and therefore if it requires editing – it’s easy to do.
Isolation Technology: How does it work?
Typemock Isolator uses the .NET Framework profiler API to intercept and monitor an application’s execution. When a method is loaded by the CLR for the test process, Typemock Isolator retrieves the IL and weaves in instrumented IL code. Typemock Isolator does not change the original IL code – it simply inserts new code that checks if the method should be faked, and with which behavior.
With this technology, Typemock Isolator can do powerful stuff:
- Fake any .Net code, including framework objects like SharePoint and ASP.Net.
- Can return fake values, ignore, throw an exception or even run custom code when a method is called.
- Return Recursive Fakes that fake full object trees in a single line.
- Tests require short setup and are more readable
- Tests are robust and don’t break easily because of production code changes.
In this article, we introduced the isolation pattern, and where it helps the most. Typemock Isolator is a powerful isolation framework you can use to create unit tests that are readable and effective.