BlackWaspTM

This web site uses cookies. By using the site you accept the cookie policy.This message is for compliance with the UK ICO law.

Testing
.NET 3.5+

Using Mocks

The fourteenth part of the Automated Unit Testing tutorial concludes the description of isolating a class from its dependencies using test doubles. This article explains how mock objects can be used to fake large classes and to test expected behaviour.

Setting Up Return Values

For the first example we'll create a test of the BalanceCalculator's GetBalance method that sums transactions for a single period. We will create a mock object to take the place of the ITransactionRetriever and program it with a set of transactions to return. This will make the mock behave in a similar manner to a stub object.

In Moq, mock objects are created using the generic Mock class. The type of the object being mocked is provided as the type parameter. So we can create a mock transaction retriever as follows:

var mockRetriever = new Mock<ITransactionRetriever>();

We can program the mock object using its Setup method. This accepts an argument containing a lambda expression that defines the expected action. In our test we will be calling the GetTransactions method for period 1, so the lambda expression that defines the expectation will be "m => m.GetTransactions(1)".

Once we have defined the expected action, we can specify the result for that call. For our test we want the method to return a predetermined array of decimal values so that we can generate a known total and assert that it is correct. Moq uses a fluent interface, so we can add our return value by appending the Returns method and providing the required result to its parameter.

The final expectation is as follows:

mockRetriever.Setup(m => m.GetTransactions(1)).Returns(new decimal[] { 1, 2, 3, 4 });

We can now create a BalanceCalculator object, passing the proxy object using constructor injection. The proxy object is obtained by reading the Object property of the Mock. A common mistake is to try to pass the Mock object itself. This will cause a compilation error in most circumstances.

var calculator = new BalanceCalculator(mockRetriever.Object);

Finally we can add an assertion that performs the calculation and checks the result. The following calculates the balance for the four transactions, which total 10, and a starting balance of 5, asserting that the result should be 15.

Assert.AreEqual(15, calculator.GetBalance(5, 1));

The complete code for the test is as follows:

[Test]
public void TotalIsCorrectForAPeriodWithTransactions()
{
    var mockRetriever = new Mock<ITransactionRetriever>();
    mockRetriever.Setup(m => m.GetTransactions(1)).Returns(new decimal[] { 1, 2, 3, 4 });
    var calculator = new BalanceCalculator(mockRetriever.Object);
    Assert.AreEqual(15, calculator.GetBalance(5, 1));
}

Setting Up Multiple Expectations

There is no restriction to the number of expectations that can be created for a mock object or, indeed, for one method or property of that object. We can take advantage of this to test the method that generates a total for several periods. In the code below we are testing the calculation of the balance of an account over a three period range. To enable this, we have three expectations, one for each period of transactions that will be retrieved:

[Test]
public void TotalIsCorrectForMultplePeriodsWithTransactions()
{
    var mockRetriever = new Mock<ITransactionRetriever>();
    mockRetriever.Setup(m => m.GetTransactions(1)).Returns(new decimal[] { 1, 2, 3, 4 });
    mockRetriever.Setup(m => m.GetTransactions(2)).Returns(new decimal[] { 5, 6, 7 });
    mockRetriever.Setup(m => m.GetTransactions(3)).Returns(new decimal[] { 8, 9, 10 });
    var calculator = new BalanceCalculator(mockRetriever.Object);
    Assert.AreEqual(60, calculator.GetBalance(5, 1, 3));
}

Throwing Exceptions from Mock Objects

In the class under test we have a try / catch block. This is used to capture an ArgumentException thrown by the data retriever class when an invalid period number is used. As we are mocking the data retriever, it is useful to have the proxy object throw an exception to test this path through the code. This is achieved by creating the same expectation but replacing the Returns call with a call to Throws. The exception that we wish to throw is provided as an argument.

We can demonstrate this with the next code sample. Here the mock object is set up to expect a call to GetTransactions for period zero. When the call is made, an ArgumentException is thrown. The assertion ensures that the exception is captured and the method returns the starting balance, as if there were no transactions for the period.

[Test]
public void TotalIsZeroForAnInvalidPeriod()
{
    var mockRetriever = new Mock<ITransactionRetriever>();
    mockRetriever.Setup(m => m.GetTransactions(0)).Throws(new ArgumentException());
    var calculator = new BalanceCalculator(mockRetriever.Object);
    Assert.AreEqual(5, calculator.GetBalance(5, 0));
}

Matching Any Value

In some testing scenarios you will not know the values that will be passed to the methods of mock objects. Sometimes it is more useful to say that one or more parameters can be any value to trigger the mock object to return the desired value or throw the specified exception. Moq allows you to ignore an argument entirely when determining if an expectation has been met. To do so, the parameter in the lambda expression is replaced with a call to the IsAny method of the static It class. This is a generic method, so the type of object to match is provided in the type parameter. Where a parameter can accept multiple types, you can set up multiple expectations to cover all types.

In the following example we are testing the calculator method for multiple periods. This time only one expectation is set up, matching the argument provided during the test run with "Is.IsAny<decimal>()". This means that whenever the GetTransactions method is called with a decimal parameter, the mock object will return the same array of four values. In the test the method will be called three times, giving a transaction total of 30 and an overall total of 35 when including the starting balance. This is verified in the assertion.

[Test]
public void TotalIsCorrectForMultipleMatchingPeriodsWithTransactions()
{
    var mockRetriever = new Mock<ITransactionRetriever>();
    mockRetriever.Setup(m => m.GetTransactions(It.IsAny<int>())).Returns(
        new decimal[] { 1, 2, 3, 4 });
    var calculator = new BalanceCalculator(mockRetriever.Object);
    Assert.AreEqual(35, calculator.GetBalance(5, 1, 3));
}
15 May 2011