[Note: This is not a continuation of my previous post, treat it as an experiment out in the wild. ] Lets consider the following class, a fictitious Fund Transfer Service: public class FundTransferService : IFundTransferService
{
private readonly ICurrencyConvertionService currencyConvertionService;
public FundTransferService(ICurrencyConvertionService currencyConvertionService)
{
this.currencyConvertionService = currencyConvertionService;
}
public void Transfer(Account fromAccount, Account toAccount, decimal amount)
{
decimal convertionRate = currencyConvertionService.GetConvertionRate(fromAccount.Currency, toAccount.Currency);
decimal convertedAmount = convertionRate * amount;
fromAccount.Withdraw(amount);
toAccount.Deposit(convertedAmount);
}
}
public class Account
{
public Account(string currency, decimal balance)
{
Currency = currency;
Balance = balance;
}
public string Currency { get; private set; }
public decimal Balance { get; private set; }
public void Deposit(decimal amount)
{
Balance += amount;
}
public void Withdraw(decimal amount)
{
Balance -= amount;
}
}
We can write the spec with MSpec + Moq like the following:
public class When_fund_is_transferred
{
const decimal ConvertionRate = 1.029m;
const decimal TransferAmount = 10.0m;
const decimal InitialBalance = 100.0m;
static Account fromAccount;
static Account toAccount;
static FundTransferService fundTransferService;
Establish context = () =>
{
fromAccount = new Account("USD", InitialBalance);
toAccount = new Account("CAD", InitialBalance);
var currencyConvertionService = new Moq.Mock<ICurrencyConvertionService>();
currencyConvertionService.Setup(ccv => ccv.GetConvertionRate(Moq.It.IsAny<string>(), Moq.It.IsAny<string>())).Returns(ConvertionRate);
fundTransferService = new FundTransferService(currencyConvertionService.Object);
};
Because of = () =>
{
fundTransferService.Transfer(fromAccount, toAccount, TransferAmount);
};
It should_decrease_from_account_balance = () =>
{
fromAccount.Balance.ShouldBeLessThan(InitialBalance);
};
It should_increase_to_account_balance = () =>
{
toAccount.Balance.ShouldBeGreaterThan(InitialBalance);
};
}
and if you run the spec it will give you a nice little output like the following:
When fund is transferred
» should decrease from account balance
» should increase to account balance
2 passed, 0 failed, 0 skipped, took 1.14 seconds (MSpec).
Now, lets see how we can write exact spec in RSpec.
require File.dirname(__FILE__) + "/../FundTransfer/bin/Debug/FundTransfer"
require "spec"
require "caricature"
describe "When fund is transferred" do
Convertion_Rate = 1.029
Transfer_Amount = 10.0
Initial_Balance = 100.0
before(:all) do
@from_account = FundTransfer::Account.new("USD", Initial_Balance)
@to_account = FundTransfer::Account.new("CAD", Initial_Balance)
currency_convertion_service = Caricature::Isolation.for(FundTransfer::ICurrencyConvertionService)
currency_convertion_service.when_receiving(:get_convertion_rate).with(:any, :any).return(Convertion_Rate)
fund_transfer_service = FundTransfer::FundTransferService.new(currency_convertion_service)
fund_transfer_service.transfer(@from_account, @to_account, Transfer_Amount)
end
it "should decrease from account balance" do
@from_account.balance.should be < Initial_Balance
end
it "should increase to account balance" do
@to_account.balance.should be > Initial_Balance
end
end
I think the above code is self explanatory, treat the require(line 1- 4) statements as the add reference of our visual studio projects, we are adding all the required libraries with this statement. Next, the describe which is a RSpec keyword. The before does exactly the same as NUnit's Setup or MsTest’s TestInitialize attribute, but in the above we are using before(:all) which acts as ClassInitialize of MsTest, that means it will be executed only once before all the test methods. In the before(:all) we are first instantiating the from and to accounts, it is same as creating with the full name (including namespace) like fromAccount = new FundTransfer.Account(.., ..), next, we are creating a mock object of ICurrencyConvertionService, check that for creating the mock we are not using the Moq like the MSpec version. This is somewhat an interesting issue of IronRuby or maybe the DLR, it seems that it is not possible to use the lambda expression that most of the mocking tools uses in arrange phase in Iron Ruby, like:
currencyConvertionService.Setup(ccv => ccv.GetConvertionRate(Moq.It.IsAny<string>(), Moq.It.IsAny<string>())).Returns(ConvertionRate);
But the good news is, there is already an excellent mocking tool called Caricature written completely in IronRuby which we can use to mock the .NET classes. May be all the mocking tool providers should give some thought to add the support for the DLR, so that we can use the tool that we are already familiar with. I think the rest of the code is too simple, so I am skipping the explanation.
Now, the last thing, how we are going to run it with RSpec, lets first install the required gems. Open you command prompt and type the following:
igem sources -a http://gems.github.com
This will add the GitHub as gem source.
Next type:
igem install uuidtools caricature rspec
and at last we have to create a batch file so that we can execute it in the Notepad++, create a batch like in the IronRuby bin directory like my previous post and put the following in that batch file:
@echo off
cls
call spec %1 --format specdoc
pause
Next, add a run menu and shortcut in the Notepad++ like my previous post. Now when we run it it will show the following output:
When fund is transferred
- should decrease from account balance
- should increase to account balance
Finished in 0.332042 seconds
2 examples, 0 failures
Press any key to continue . . .
You will complete code of this post in the bottom.
That's it for today.
Download: RSpecIntegration.zip