So let’s say I have a user story like:
As a Portfolio Analyst
I need to be able to change an account’s market value, overriding what was reported by the custodian
So I can reflect cash flows I know are going to hit the account.
So we talk with them and know that we are going to make the Market Value field on some screen editable. When they don’t do anything, we will just use the market value reported by the custodian (which is what we already do now) but when they enter a manual plug, we will use that value instead.
So the developers talk about it an decide we first want to introduce properties ReportedMarketValue and EstimatedMarketValue. Here is the original Account class
public class Account
{ public virtual string AccountNumber { get; set; } public virtual decimal MarketValue { get; set; }}
Which we change to:
public class Account
{ public virtual string AccountNumber { get; set; } public virtual decimal MarketValue { get; set; } public virtual decimal ReportedMarketValue { get; set; } public virtual decimal? EstimatedMarketValue { get; set; }}
Next we want to make MarketValue read-only, have it either be EstimatedMarketValue if there is one, or ReportedMarketValue. All the places that don’t compile anymore (where someone said “account.MarketValue = “) will be changed to use the ReportedMarketValue mutator.
…but before that, let’s get our specs in place.
MSpec makes heavy use of lambdas, fluent interface, and a custom nunit test runner to take away a lot of the curly braces. Again, read Aaron’s blog for the introduction, but basically, we are going to declare a base class class to establish all the actors in a series of specs around this story:
public abstract class account_spec
{ protected static Account Account { get; private set; }
Establish context = () => { Account = new Account();
Account.ReportedMarketValue = 100M;
};
}
Establish is just a delegate and we’re setting it to an anonymous method to just create a new Account. Notice that the Account property is static? Yeah, that sucks. You can’t reference non static members from inside an initializer for a delegate:
I think the driving design choice is to limit the amount of alien noise in the spec. Here the tradeoff is adding “static” vs. doing the initialization inside of a constructor or something. Tough call. Whatever, this works, so let’ just go with it.
Next we want to specify what happens when there is no plugged market value:
[Concerning(typeof(Account), "Market Value")]
public class with_no_plugged_market_value : account_spec
{ Because the_account_has_no_plugged_market_value = () => Account.EstimatedMarketValue = null;
It should_give_the_reported_market_value_as_the_market_value = () =>
Account.MarketValue.ShouldEqual(Account.ReportedMarketValue);
}
There is more decoration here. Let’s go over that:
- The Concerning Attribute: Is used to format the output of the test run and to introduce a “test fixture”.
- Because: is just another anonymous delegate. I think in one of my experiments I tried having two because’s (I was thinking it be like “because of this and because of that”) but only one got executed. This makes sense. Just put all your because stuff in the same block.
- It: is…yep, just another anonymous delegate. You can have multiple It’s.
- ShouldEqual: is a nice extension method that comes with mspec. Thanks!
When run, this spec fails:
------ Test started: Assembly: Specs.dll ------
Account Market Value, with no plugged market value
!!» should give the reported market value as the market value !!
TestCase 'should give the reported market value as the market value' failed:
NUnit.Framework.AssertionException: Expected: 100m
But was: 0m
at NUnit.Framework.Assert.That(Object actual, Constraint constraint, String message, Object[] args)
at NUnit.Framework.Assert.AreEqual(Object expected, Object actual)
C:\projects\machine\Source\Specifications\Machine.Specifications\ExtensionMethods.cs(25,0): at Machine.Specifications.ShouldExtensionMethods.ShouldEqual(Object actual, Object expected)
C:\Users\cbilson\Documents\Visual Studio 2008\Projects\ClassLibrary2\Specs\Class1.cs(24,0): at Specs.with_no_plugged_market_value.<.ctor>b__1()
0 passed, 1 failed, 0 skipped, took 0.54 seconds.
So we implement it (Spec First Development!):
public virtual decimal MarketValue { get { return ReportedMarketValue; } set { throw new NotImplementedException(); }}
(don’t worry, we’ll get rid of set later in another refactoring – or forget about it and find a zillion other tests broke.)
Now we pass:
------ Test started: Assembly: Specs.dll ------
Account Market Value, with no plugged market value
» should give the reported market value as the market value
1 passed, 0 failed, 0 skipped, took 0.46 seconds.
Note the nice Product-Owner-friendly output. This was one of my bullets from Part 1. Check.
Remember MTEGOTAG? It turns out that this increases my MTEGOTAG number to about 5 minutes. After that the product owner’s eyes glaze over and their “thinking about leaving early for a round of golf” program pegs their CPU. They can still search this output though if they have a question about something. But from a mean of around 13 seconds to 5 minutes is an amazing feat!
The other spec:
[Concerning(typeof(Account), "Market Value")]
public class with_plugged_market_value : account_spec
{ Because the_account_has_plugged_market_value_of_200 = () => Account.EstimatedMarketValue = 200M;
It should_give_the_estimated_market_value_as_the_market_value = () =>
Account.MarketValue.ShouldEqual(Account.EstimatedMarketValue);
}
Which fails until we implement it:
public virtual decimal MarketValue { get { return EstimatedMarketValue ?? ReportedMarketValue; } set { throw new NotImplementedException(); }}
And all our specs run together look like:
------ Test started: Assembly: Specs.dll ------
Account Market Value, with no plugged market value » should give the reported market value as the market value
Account Market Value, with plugged market value » should give the estimated market value as the market value
2 passed, 0 failed, 0 skipped, took 0.46 seconds. |
This can almost go straight into our trac wiki. Maybe someone I work with will read this and fix our build scripts to update trac.
One quick note: When you run the spec project in TD.NET, it doesn’t show the output of all the specs, just how many failed/succeeded. I could probably fix this and submit a patch. Hopefully I will.
Final things to note: There are a lot of conventions in play here – ex.: the concern’s name and the spec’s name and how they get formatted in the end. I like conventions because it means there’s less to explain to get going. I can see new developers being able to easily learn write specs like this, but I bet they are going to run into a lot of bizarre compiler errors along the way. Good way for them to learn about lambdas and c#!
So, apologies for the lame - yet long - and simplistic example. This is almost like something we did on a real project at work, just way simpler. Now that we have a simple example to start with, in my next post, I want to go over a more complicated example – one in which I felt a little bit more pain and confusion. That’s going to take me longer to type up though. In the meantime, please comment. Thanks!