In this example we'll implement part of a banking system. We need to be able to transfer money between two accounts owned by the same customer, but which have different currencies. During the transfer, we'll need to obtain the current exchange rate and use it to convert the funds. Please note that this is a deliberately simplified example—in a real-world application you might not choose to use double to store something important like money, or to use simple strings for currencies.
Let's start with the service we wish to test, IAccountService. We start with a single method:
public interface IAccountService { void TransferFunds(Account from, Account to, double amount); }
For this simplified example, two Account objects are passed to the service and we'll assume they have already been populated with the current account balances, currencies, etc. A more realistic example might just pass account numbers to the service, which would then need to do some data access to load the accounts. Let's write a skeletal implementation of the service:
public class AccountService : IAccountService { public void TransferFunds(Account from, Account to, double amount) { from.Withdraw(amount); to.Deposit(amount); } }
The obvious problem here is that we're ignoring the currencies—if we do transfers between accounts that have different currency codes, the bank will leak money! We know we need to do currency conversion, so let's make our service depend on a new currency service:
public interface ICurrencyService { double GetConversionRate(string fromCurrency, string toCurrency); }
public class AccountService : IAccountService { private readonly ICurrencyService currencyService; public AccountService(ICurrencyService currencyService) { this.currencyService = currencyService; } ... }
Here we're using constructor dependency injection to provide the currency service to the account service. Instead of going off into the environment and somehow finding that currency service, we provide it directly in the constructor. This makes the account service easier to test because we can replace the real currency service with a mock object.
Now we can create an NUnit test for our currency conversion service. In our SetUp method we'll create a Mockery, the basic factory for mock objects. Next we'll create a mock currency service and pass it to our account service (the class we're actually trying to test).
using NUnit.Framework; using NMock2; [TestFixture] public class CurrencyServiceTest { private Mockery mocks; private ICurrencyService mockCurrencyService; private IAccountService accountService; [SetUp] public void SetUp() { mocks = new Mockery(); mockCurrencyService = mocks.NewMock<ICurrencyService>(); accountService = new AccountService(mockCurrencyService); } }
We know that our account service should use the GetConversionRate() method to find the conversion rate between two bank accounts, and that it should adjust the amount being moved between the accounts accordingly. The test might look like this:
[Test] public void ShouldUseCurrencyServiceToDetermineConversionRateBetweenAccounts() { Account canadianAccount = new Account("12345", "CAD"); Account britishAccount = new Account("54321", "GBP"); britishAccount.Deposit(100); Expect.Once.On(mockCurrencyService). Method("GetConversionRate"). With("GBP", "CAD"). Will(Return.Value(2.20)); accountService.TransferFunds(britishAccount, canadianAccount, 100); Assert.AreEqual(0, britishAccount.Balance); Assert.AreEqual(220, canadianAccount.Balance); mocks.VerifyAllExpectationsHaveBeenMet(); }
We created two accounts containing British and Canadian funds, and started by depositing 100 pounds in the British account. Then we told the mock currency service to expect the method GetConversionRate() with arguments GBP and CAD, and that the mock object should return 2.20. Right in the middle of the test we call the TransferFunds() method on the account service to transfer 100 pounds from the British account into the Canadian account. Next, we assert that the British account ends up empty and the Canadian account ends up with 220 dollars. Finally, we use the VerifyAllExpectationsHaveBeenMet() to ensure that all of the expected methods were called on our mock currency service.
Sadly, this test fails. Our first (money-leaking) implementation of the transfer method didn't know anything about currencies. Let's fix that and get our test to pass by using the currency service to determine the conversion rate:
public void TransferFunds(Account from, Account to, double amount) { from.Withdraw(amount); double conversionRate = currencyService.GetConversionRate(from.Currency, to.Currency); double convertedAmount = amount * conversionRate; to.Deposit(convertedAmount); }
Our test passes and we can go home for the night, confident that our account transfer service correctly performs currency conversion. But what have we really achieved by using the mock currency service?
- our test only exercises the account service code, so if the test breaks, we know something in the account service is wrong.
- our test is reliable, because the mock currency service always returns an exchange rate of 2.20. If we'd used a real currency service—based on a database, a real-time web service feed, etc.—we wouldn't know what exchange rate it was going to return, so couldn't assert that the Canadian bank account should end up with $220.
- our test is actually driving out some of the behaviour of the rest of the system, by defining the interface to some of the other components. In this case we've defined how the GetConversionRate() method should look and can test the account service's use of it, without ever needing to actually implement the currency service. This style of testing is popular with test driven development.
NMock contains many more ways of specifying expected behaviour, defining return values and exceptions that might be thrown, and asserting that the classes we're testing behave correctly. To learn more please see the Tutorial or the handy cheat-sheet.