Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / C#

Stop Designing for Testability

4.78/5 (46 votes)
4 Apr 20079 min read 1  
An article presenting a different technique to making your code more testable.

Contents

Introduction

With 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 Validate

The 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.

C#
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 Transaction?

Isolating Transaction

The first thing that we are going to do is to write our mock Transaction, one that doesn't really access a database. To do so, we are going to create an interface for Transaction and create our own MockTransaction. In the following example, we will create a manual mock, but you can use any mock framework.

C#
using System;
namespace ATM
{
    public interface ITransaction
    {
        void Execute();
        bool IsSuccessful { get; }
    }
}

And our MockTransaction:

C#
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 Transaction to MockTransaction in Teller.Withdraw(), we could then run our test and verify that our code works well when the transaction succeeds. The techniques that we are going to see are part of Dependency Injection or Inversion of Control Techniques.

Extract and Override Pattern

This is the simplest way to isolate a Transaction. We extract the creation command to a virtual method, and we override this method for the test. Here is what our code now looks like. Notice the use of ITransaction and the CreateTransaction virtual method.

C#
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 MockTransaction, we are going to have to create a new class that is derived from Teller. In this class, we will override the creation method and swap it with our own MockTransaction. Here is our test:

C#
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 Transaction, but if we create a transaction in multiple places, we are going to have code duplication (many CreateTransaction methods). A better way in this scenario is to use an Abstract Factory.

Introduce Abstract Factory

In 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 ITransaction, and a Factory that will call the correct Creator and return our newly made ITransaction. The Factory with a method to change the creator may look as follows:

C#
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:

C#
namespace ATM
{
    public interface ITransactionCreator
    {
        ITransaction Create(Account account, decimal amount);
    }
}

We will have a default creator:

C#
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 MockCreator:

C#
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 TransactionFactory.CreateTransaction call.

C#
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 MockCreator. Note that we now use the normal Teller.

C#
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 TransactionFactory doesn't know to use the TransactionCreator by default, but I will leave this as an exercise for the readers.

For this pattern, we created one Factory, two interfaces, one Creator class, and one property used only for tests (TransactionFactory.Creator needed to swap the creator class) in our production code, and one mock object and creator for our tests. (Some implementations will require an additional configuration needed to load the default TransactionCreator.)

The Price of Isolation

Using 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 Transaction class easily. There is a price to having this amount of flexibility. Let's see what the price is for this.

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 Transaction class? How do we swap the creation to create a mocked object?

Here, some modern tools come very handy.

Using TypeMock.NET to isolate our code

This 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 Transaction class without changing our design at all. All we do is tell the framework that when a new Transaction() is called, swap it with a mocked instance.

Here is an example that will isolate the Transaction and call the mocked code.

C#
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:

  1. While we are in the using block, all methods will be recorded and mocked.
  2. We create a Transaction, meaning that we will mock the next Transaction that will be created.
  3. We also validate that the arguments passed to the constructor are our account and that we are removing $100 from the account.
  4. As we don't want to actually execute the transaction we mock the Execute method and the IsSuccessful property.
  5. In this test we will check that our code works OK for successful transactions.

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.

C#
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 Methods

Using 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 TransactionPool that will give us Transactions. Our code will now look as follows:

C#
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:

C#
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 calls

TypeMock.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 TransactionPool can be reached from a DataAccess that is a Singleton. Our code will now look as follows:

C#
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:

C#
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 work

TypeMock.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.

Trace Example

Conclusion

Although 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 History

July 30, 2006

  • First post.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here