This post discusses the RowTest / Theory / TestData support for the amazing specification framework Machine.Specifications. For those who don’t know Machine.Specifications (aka MSpec) I strongly advice to check it out here. MSpec has one caveat which can sometimes make your life a bit harder than it should be: It misses RowTest / Theory (xUnit) or TestData (nunit) support. With row test support I mean the ability to execute the same Establish, Because and It for several runs with slightly different input arguments. Urs Enzler, Philipp Dolder and I discussed how an API for MSpec could look like during a coffee break and Urs Enzler came up with a nice API idea which is described here. I couldn’t resist that idea an began to implement it over the weekend. The initial draft can be found here. Basically it allows you to define examples which then run the same Establish, Because and It delegates for each example. The beauty is that you can still define the normal delegates you are used to if a particular It or Establish doesn’t care about the example being executed.
The syntax looks like the following:
[Subject(typeof(Account), "Funds transfer example")] public class when_transferring_between_two_accounts_with_examples { static Account fromAccount; static Account toAccount; Establish<Transfer> accounts = transfer => { fromAccount = new Account { Balance = transfer.FromAccountBalanceBeforeTransfer }; toAccount = new Account { Balance = transfer.ToAccountBalanceBeforeTransfer }; }; Examples<Transfer> transfers = () => { return new[] { new Transfer { Amount = 1m, FromAccountBalanceBeforeTransfer = 1m, ToAccountBalanceBeforeTransfer = 1m, FromAccountBalanceAfterTransfer = 0m, ToAccountBalanceAfterTransfer = 2m }, new Transfer { Amount = 50m, FromAccountBalanceBeforeTransfer = 100m, ToAccountBalanceBeforeTransfer = 100m, FromAccountBalanceAfterTransfer = 50m, ToAccountBalanceAfterTransfer = 150m }, }; }; Because<Transfer> transfer_is_made = transfer => fromAccount.Transfer(transfer.Amount, toAccount); It<Transfer> should_debit_the_from_account_by_the_amount_transferred = transfer => fromAccount.Balance.ShouldEqual(transfer.FromAccountBalanceAfterTransfer); It<Transfer> should_credit_the_to_account_by_the_amount_transferred = transfer => toAccount.Balance.ShouldEqual(transfer.ToAccountBalanceAfterTransfer); public class Transfer { public decimal FromAccountBalanceBeforeTransfer { get; set; } public decimal ToAccountBalanceBeforeTransfer { get; set; } public decimal FromAccountBalanceAfterTransfer { get; set; } public decimal ToAccountBalanceAfterTransfer { get; set; } public decimal Amount { get; set; } public override string ToString() { return string.Format( "Transfering {0} from account with initial balance {1} to account with initial balance {2}", Amount, FromAccountBalanceBeforeTransfer, ToAccountBalanceBeforeTransfer); } } }
Custom Delegates are also supported:
[Subject(typeof(Account), "Funds transfer example")] public class when_transferring_between_two_accounts_with_examples { static Account fromAccount; static Account toAccount; Given<Transfer> accounts = transfer => { fromAccount = new Account { Balance = transfer.FromAccountBalanceBeforeTransfer }; toAccount = new Account { Balance = transfer.ToAccountBalanceBeforeTransfer }; }; Examples<Transfer> transfers = () => { return new[] { new Transfer { Amount = 1m, FromAccountBalanceBeforeTransfer = 1m, ToAccountBalanceBeforeTransfer = 1m, FromAccountBalanceAfterTransfer = 0m, ToAccountBalanceAfterTransfer = 2m }, new Transfer { Amount = 50m, FromAccountBalanceBeforeTransfer = 100m, ToAccountBalanceBeforeTransfer = 100m, FromAccountBalanceAfterTransfer = 50m, ToAccountBalanceAfterTransfer = 150m }, }; }; When<Transfer> transfer_is_made = transfer => fromAccount.Transfer(transfer.Amount, toAccount); Then<Transfer> should_debit_the_from_account_by_the_amount_transferred = transfer => fromAccount.Balance.ShouldEqual(transfer.FromAccountBalanceAfterTransfer); Then<Transfer> should_credit_the_to_account_by_the_amount_transferred = transfer => toAccount.Balance.ShouldEqual(transfer.ToAccountBalanceAfterTransfer); public class Transfer { public decimal FromAccountBalanceBeforeTransfer { get; set; } public decimal ToAccountBalanceBeforeTransfer { get; set; } public decimal FromAccountBalanceAfterTransfer { get; set; } public decimal ToAccountBalanceAfterTransfer { get; set; } public decimal Amount { get; set; } public override string ToString() { return string.Format( "Transfering {0} from account with initial balance {1} to account with initial balance {2}", Amount, FromAccountBalanceBeforeTransfer, ToAccountBalanceBeforeTransfer); } } }
And the current supported console or html reports look like:
and html
Because I didn’t want to introduce breaking changes on the reporters I decided to simply simulate multiple contexts in case you have examples on a single context. Currently I choose to output the following name:
Concern [Example: ToString of Example]
There is still a lot to do:
- Resharper support
- Cleanup
- Heavy testing for edge cases
- …
How do you like that feature? I would like to hear your opinion! Join the discussion on the github issue page for MSpec.
RT @planetgeekch: RowTest / Theory / TestData support for Machine.Specifications #mspec http://t.co/uneiuXplnR
@UliArmbruster http://t.co/DVtTPNtycG
MSpec will support examples soon thanks to @danielmarbach and colleagues http://t.co/rKjkbS1y0b
RT @agross: MSpec will support examples soon thanks to @danielmarbach and colleagues http://t.co/rKjkbS1y0b
RT @planetgeekch: RowTest / Theory / TestData support for Machine.Specifications #mspec http://t.co/uneiuXplnR
RT @danielmarbach: @UliArmbruster http://t.co/DVtTPNtycG
[…] The new way is different. We use the same approach as xunit V1 uses. There is a runner abstraction called runner utility. The runner utility is the stable abstraction which all runner infrastructure binds against. Using some crazy app domain marshaling and remoting magic the runner utility establishes an implicit contract with the core (Machine.Specifications) without having a direct dependency to the core and its version. This allowed us to completely seperate all the different moving parts of Machine.Specifications into seperate repository and in the future evolve the versions of those parts individually too. There is one exception though: There will be a breaking change in the runner utility and forcing you to update again eventually when we pull in the examples support. […]