Write your Tests in RSpec with IronRuby

[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 ICurrencyConversionService currencyConversionService;

	public FundTransferService(ICurrencyConversionService currencyConversionService)
	{
		this.currencyConversionService = currencyConversionService;
	}

	public void Transfer(Account fromAccount, Account toAccount, decimal amount)
	{
		decimal conversionRate = currencyConversionService.GetConversionRate(fromAccount.Currency, toAccount.Currency);
		decimal convertedAmount = conversionRate * 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 ConversionRate = 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<ICurrencyConversionService>();
		currencyConvertionService.Setup(ccv => ccv.GetConversionRate(Moq.It.IsAny<string>(), Moq.It.IsAny<string>())).Returns(ConversionRate);

		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 the exact spec in RSpec.

require File.dirname(__FILE__)  + "/../FundTransfer/bin/Debug/FundTransfer"
require "spec"
require "caricature"

describe "When fund is transferred" do

	Conversion_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_conversion_service = Caricature::Isolation.for(FundTransfer::ICurrencyConversionService)
		currency_conversion_service.when_receiving(:get_Conversion_Rate).with(:any, :any).return(Conversion_Rate)

		fund_transfer_service = FundTransfer::FundTransferService.new(currency_conversion_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 ICurrencyConversionService, 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 get the complete code of this post in the following link.

That's it for today.

Download: RSpecIntegration.zip

Shout it

2 Comments

  • Very nice writeup and comparison to mspec, which is my C# test tool of choice as well. Very interesting experiment.

    I've been looking for ways to integrate IronRuby into my development environment. I initially thought that testing with rspec was going to be a real sweet spot. But I don't think it is yet, not at least for TDD.

    With my current toolset (R#, TestDriven) TDD is extremely easy. I can create my test first, slinging new classes and properties around like crazy and let R# fill in the gaps. Then let TDD.Net run the tests in just a few keystrokes.

    Plus, since we still need a Mocking tool for CLR types, we don't get any advantages of duck punching our classes as we would with true Ruby classes. You've shown that it takes as many lines of code and as much work to create the test with rspec. (Is this true for .Net 4?)

    Until we have better integration with IR, I fear productivity would decrease instead of decrease.

    I'm still bullish on using IronRuby, I currently building an IR repl for my app (not hard just not much time) that lests me script instead of ad-hoc db functions. Hopefully I'll blog about my findings.

  • @John: Yes at the moment MSpec looks a lot frictionless and Mocking clr type is must if we want to go with RSpec.

Comments have been disabled for this content.