RowTest / Theory / TestData support for Machine.Specifications

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:

ConsoleReport

and html

HtmlReport

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.

About the author

Daniel Marbach

7 comments

Recent Posts