|
|||||||||||||||||||||
|
|||||||||||||||||||||
|
Announcements
Services
Chapters
Feature Zones
|
Contents
IntroductionWith over 35,000 downloads of Nunit Framework per month (see here), there is a noticeable trend that Test Driven Development and the values of having a set of Automated Tests are worth the price. Even Microsoft has joined the game and is distributing its own testing framework (we will actually use this framework in the examples of this article). There are many articles about Test Driven Development, but once you start writing tests, you will notice that you start to design your code to become testable. In this article, we will see that these techniques used to create testable code, unnecessarily make your code complex and thus hard to maintain. This is something which is opposite to the Test Driven Way of Creating the Simplest thing that will work. Isolate to ValidateThe goal of unit testing is to isolate each part of the program and show that the individual parts are correct. Although this goal is easy to understand, it is quite hard to implement. This is where Mocks and Mocking Frameworks come in handy. To see a good explanation, see Mark Seemann's Mock Objects to the Rescue! Test Your .NET Code with NMock article. The basic idea of mocks is to intercept the normal flow of the code and to insert dummy objects. These objects are not so dumb, and their behavior can be programmed and verified; this includes verifying the arguments passed, returning different values, and validating that the expected calls where actually made. All this is fine and will work, but it leaves us with a problem: in order to intercept the normal flow, we are going to have to somehow change the real objects with mock ones. The normal way to do this is to change the design of your program to allow object swapping. We will see that this leads to creating virtual methods or new interfaces just for testing. Let's see an example: we are creating an ATM, we are going to implement a method that performs the withdrawal of our cash. Here is one way to implement this. namespace ATM
{
public class Teller
{
public bool Withdraw(Account account, decimal amount)
{
Transaction transaction = new Transaction(account, -amount);
transaction.Execute();
if (transaction.IsSuccessful)
{
Dispose(amount);
return true;
}
return false;
}
}
}
This looks simple and easy to understand, but can we test this in Isolation? How do we isolate Isolating TransactionThe first thing that we are going to do is to write our mock using System;
namespace ATM
{
public interface ITransaction
{
void Execute();
bool IsSuccessful { get; }
}
}
And our using ATM;
namespace ATM.Test
{
public class MockTransaction : ITransaction
{
public Account account;
public decimal amount;
public int executeCalledCounter = 0;
public MockTransaction(Account account, decimal amount)
{
// Save arguments for later validation
this.account = account;
this.amount = amount;
}
#region ITransaction Members
public void Execute()
{
// save number of times this has been called
executeCalledCounter++;
}
public bool IsSuccessful
{
// always be successful
get { return true; }
}
#endregion
}
}
If we could only swap the creation of Extract and Override PatternThis is the simplest way to isolate a namespace ATM
{
public class Teller
{
public bool Withdraw(Account account, decimal amount)
{
ITransaction transaction = CreateTransaction(account, -amount);
transaction.Execute();
if (transaction.IsSuccessful)
{
Dispose(amount);
return true;
}
return false;
}
// We moved the creation statment to a virtual method
// so that we are able to inject a Mock Transaction
protected virtual ITransaction CreateTransaction(Account account,
decimal amount)
{
return new Transaction(account, amount);
}
}
}
In order to inject our own using Microsoft.VisualStudio.TestTools.UnitTesting;
using ATM;
namespace ATM.Test
{
public class TellerForTest : Teller
{
// We need to reference the mock to validate our tests
public MockTransaction mockTransaction;
// Make sure that our Mocked Version of Transaction
// is created instead of the real one
protected override ITransaction CreateTransaction(Account account,
decimal amount)
{
this.mockTransaction = new MockTransaction(account,amount);
return this.mockTransaction;
}
}
[TestClass]
public class TestTeller
{
[TestMethod]
public void TestCanWithdraw()
{
TellerForTest teller = new TellerForTest();
Account account = new Account();
bool actual = teller.Withdraw(account,100);
Assert.AreEqual(true, actual);
// check that transaction was called
Assert.IsNotNull(teller.mockTransaction);
// check that correct values where passed
Assert.AreSame(account, teller.mockTransaction.account);
Assert.AreEqual(-100, teller.mockTransaction.amount);
// check that Execute was called
Assert.AreEqual(1, teller.mockTransaction.executeCalledCounter);
}
}
}
For this pattern, we created one interface and one virtual method in our production code, and one mock object and one derived class for our tests. This method works well when we have one place where we create a Introduce Abstract FactoryIn order to have a better way to insert mock objects, we can use a variation of an Abstract Factory. Here is what our code will look like. We will have some creator classes that actually create the namespace ATM
{
public class TransactionFactory
{
// our creator knows how create an ITransaction
static ITransactionCreator creator;
public static ITransactionCreator Creator
{
set { creator = value; }
}
public static ITransaction CreateTransaction(Account account,
decimal amount)
{
// ask the creator to give us a new ITransaction
return creator.Create(account, -amount);
}
}
}
Our creator interface: namespace ATM
{
public interface ITransactionCreator
{
ITransaction Create(Account account, decimal amount);
}
}
We will have a default creator: namespace ATM
{
internal class TransactionCreator : ITransactionCreator
{
#region ITransactionCreator Members
public ITransaction Create(Account account, decimal amount)
{
return new Transaction(account, amount);
}
#endregion
}
}
And for our test, we need a using ATM;
namespace ATM.Test
{
public class MockTransactionCreator : ITransactionCreator
{
// We need to reference the mock to validate our tests
public MockTransaction transaction;
#region ITransactionCreator Members
public ITransaction Create(Account account, decimal amount)
{
transaction = new MockTransaction(account,amount);
return transaction;
}
#endregion
}
}
Our code will now call the factory. Notice the namespace ATM
{
public class Teller
{
public bool Withdraw(Account account, decimal amount)
{
ITransaction transaction =
TransactionFactory.CreateTransaction(account, -amount);
transaction.Execute();
if (transaction.IsSuccessful)
{
Dispose(amount);
return true;
}
return false;
}
}
}
Our test will now tell our Factory to use the using Microsoft.VisualStudio.TestTools.UnitTesting;
using ATM;
namespace ATM.Test
{
[TestClass]
public class TestTeller
{
[TestMethod]
public void TestCanWithdraw()
{
Teller teller = new Teller();
Account account = new Account();
// insert our own Transaction
MockTransactionCreator creator = new MockTransactionCreator();
TransactionFactory.Creator = creator ;
bool actual = teller.Withdraw(account,100);
Assert.AreEqual(true, actual);
// check that transaction was called
Assert.IsNotNull(creator.transaction);
// check that correct values where passed
Assert.AreSame(account, creator.transaction.account);
Assert.AreEqual(-100, creator.transaction.amount);
// check that Execute was called
Assert.AreEqual(1, creator.transaction.executeCalledCounter);
}
}
}
Although we have done the tests, we have not finished yet because our For this pattern, we created one Factory, two interfaces, one The Price of IsolationUsing the methods explained above, we can test our code plus we get the added value of our code having a low correlation, which means that we can change the We now have many interfaces that are exact representations of the concrete class, and changes are required in both places. We have also changed our implementation and our design. This means 3-8 more classes! We have actually created a framework to help us isolate our code. This is not exactly YAGNI (You Ain't Gonna Need It), a practice that is part of Test Driven Development that says we should Do the Simplest Thing to make the tests work. Using an Abstract Factory will lead to more bugs, and will make the code harder to understand, harder to debug, and harder to maintain, all this for a feature that we need only for testing. There must be a better way. Our business features should drive us to create more complex code, not our tests. But then, how can we isolate the Here, some modern tools come very handy. Using TypeMock.NET to isolate our codeThis is where TypeMock.NET comes to the rescue. TypeMock has come with a new idea: instead of using POJO and OO ways to isolate our code, we should use other techniques that will enable us to to isolate our code. Using the magic of TypeMock.NET, we can now isolate and swap our Here is an example that will isolate the using Microsoft.VisualStudio.TestTools.UnitTesting;
using ATM;
using TypeMock;
namespace ATM.Test
{
[TestClass]
public class TestTeller
{
[TestMethod]
public void TestCanWithdraw()
{
Account account = new Account();
// Set Expectations (see note 1)
using (RecordExpectations record = RecorderManager.StartRecording())
{
// We expect the a new Transaction to be created
// with these arguments (see note 2)
Transaction mockTransaction = new Transaction(account, -100);
record.CheckArguments(); // (see note 3)
// We expect Execute to be called on the future object
// (see note 4)
mockTransaction.Execute();
// We will return a successfull transaction (see note 5)
record.ExpectAndReturn(mockTransaction.IsSuccessful, true);
}
// Lets run our tests
Teller teller = new Teller();
bool actual = teller.Withdraw(account, 100);
Assert.AreEqual(true, actual);
// make sure that all expected methods where called
MockManager.Verify();
}
[TestCleanup]
public void MyTestCleanup()
{
// Stop mocking
MockManager.CleanUp();
}
}
}
Explanation:
The attentive reader might have noticed that we don't pass any mocked object to the code that we are testing, this is because we don't need to. TypeMock.NET will automatically load the mocked object when needed. The best part is that our production code hasn't changes and is simple to understand while we are able to test it simply. But this is only part of the story. Suppose we cannot create our Transaction (the constructor is private). How do we isolate it? Here Reflective Mocks come handy. using Microsoft.VisualStudio.TestTools.UnitTesting;
using ATM;
namespace ATM.Test
{
[TestClass]
public class TestTeller
{
[TestMethod]
public void TestCanWithdraw()
{
Account account = new Account();
// We expect the a new Transaction to be created
Mock transactionMock = MockManager.Mock(typeof(Transaction));
// We expect the Transaction to be created with these arguments
transactionMock.ExpectConstructor().Args(account, -100);
// We expect Execute to be called on the future object
transactionMock.ExpectCall("Execute");
// We will return a successfull transaction
transactionMock.ExpectGet("IsSuccessful", true);
Teller teller = new Teller();
bool actual = teller.Withdraw(account, 100);
Assert.AreEqual(true, actual);
MockManager.Verify();
}
[TestCleanup()
public void MyTestCleanup()
{
// Stop mocking
MockManager.CleanUp();
}
}
}
Here we did the same as above but using Reflective Mocks, as these are reflective we pass the method names as strings, this gives us the ability to mock private methods but then we lose the power of intellisense and refactoring. Mocking Static MethodsUsing TypeMock.NET we can isolate static methods and sealed classes just as easy a feat that is quite impossible using regular techniques. Suppose that we decided that for the sake of performance we will have a namespace ATM
{
public class Teller
{
public bool Withdraw(Account account, decimal amount)
{
Transaction transaction =
TransactionPool.GetTransaction(account, -amount);
transaction.Execute();
if (transaction.IsSuccessful)
{
Dispose(amount);
return true;
}
return false;
}
}
}
We can still test this using TypeMock as follows: using Microsoft.VisualStudio.TestTools.UnitTesting;
using ATM;
using TypeMock;
namespace ATM.Test
{
[TestClass]
public class TestTeller
{
[TestMethod]
public void TestCanWithdraw()
{
Account account = new Account();
// Set Expectations
using (RecordExpectations record = RecorderManager.StartRecording())
{
// We expect the a Transaction to be fetched
// from the pool created with these arguments
// TypeMock will automatically return a mocked type.
Transaction mockTransaction =
TransactionPool.GetTransaction(account, -100);
record.CheckArguments();
// We expect Execute to be called on the future object
mockTransaction.Execute();
// We will return a successfull transaction
record.ExpectAndReturn(mockTransaction.IsSuccessful, true);
}
// Lets run our tests
Teller teller = new Teller();
bool actual = teller.Withdraw(account, 100);
Assert.AreEqual(true, actual);
// make sure that all expected methods where called
MockManager.Verify();
}
[TestCleanup]
public void MyTestCleanup()
{
// Stop mocking
MockManager.CleanUp();
}
}
}
Mocking a chain of callsTypeMock.NET goes even further and can isolate a chain of calls in one go, a feat that requires many code changes using normal mocking frameworks. Suppose that we decided that our namespace ATM
{
public class Teller
{
public bool Withdraw(Account account, decimal amount)
{
// Notice that we are calling a chain of methods
Transaction transaction =
DataAccess.Instance.GetTransactionPool().GetTransaction(account,
-amount);
transaction.Execute();
if (transaction.IsSuccessful)
{
Dispose(amount);
return true;
}
return false;
}
}
}
We can still test this using TypeMock as follows: using Microsoft.VisualStudio.TestTools.UnitTesting;
using ATM;
using TypeMock;
namespace ATM.Test
{
[TestClass]
public class TestTeller
{
[TestMethod]
public void TestCanWithdraw()
{
Account account = new Account();
// Set Expectations
using (RecordExpectations record = RecorderManager.StartRecording())
{
// We expect the a Transaction to be fetched
// from the pool created with these arguments
// TypeMock will automatically return a mocked type.
Transaction mockTransaction =
DataAccess.Instance.GetTransactionPool().GetTransaction(account,
-100);
record.CheckArguments();
// We expect Execute to be called on the future object
mockTransaction.Execute();
// We will return a successful transaction
record.ExpectAndReturn(mockTransaction.IsSuccessful, true);
}
// Lets run our tests
Teller teller = new Teller();
bool actual = teller.Withdraw(account, 100);
Assert.AreEqual(true, actual);
// make sure that all expected methods where called
MockManager.Verify();
}
[TestCleanup]
public void MyTestCleanup()
{
// Stop mocking
MockManager.CleanUp();
}
}
}
As we can see although we have isolated our code, it is still easy to refactor while keeping the tests intact. If at some time our code does require the ability to change the Transaction, for example a customer asked for an ATM that can work with flat files instead of a database we can re-factor our code to use one of the above techniques. In most cases, we won't even have to change our test code. How does this magic workTypeMock.NET uses Aspect Oriented Programming, a new Methodology that allows mixing classes and joining them in different positions. What TypeMock does is actually isolate the real code and decide when the real code should be run and when it should be mocked. Using the Profiler API, TypeMock.NET monitors the creations and usages of the code and then Instruments the code to enable isolation when needed. To see how this happens, TypeMock.NET supplies a Trace that shows all the instances and methods of mocked types are called. ConclusionAlthough creating interfaces and using Factories is considered a good O/O practice, if taken to the extreme it clutters our code. We should be able to decide what the best design is for the production code without changing it to make our code testable. In the past this was not possible but with modern tools like TypeMock.NET this is feasible, so we should STOP kidding ourselves, there is no need to change our design to make our code testable. The Production Code and features should drive our Design not our tests. This will save us hours of developing and maintaining unneeded code. I of course would like to discuss this opinion. References
Just to clear things up, I am associated with TypeMock.NET but that is only because I really believe in the Tool and the Methodology behind it. Revision HistoryJuly 30, 2006
| ||||||||||||||||||||