Acceptance Tests for User Stories allow the Product Owner to more easily say whether or not they accept a Story as ‘Done’. Also, Acceptance Tests can be used in Behaviour Driven Development (BDD) to provide an “outside in” development process that complements the “inside out” coding style of Test Driven Development (TDD).
SpecFlow brings Cucumber BDD to .NET without the need for a Ruby intermediary like IronRuby.
In your Tests project add a Features folder. SpecFlow installs some templates into VS.NET so add a new SpecFlowFeature and fill it out like the following example, InterestRatings.feature :
Feature: Interest Ratings In order to manage the Interest Ratings As a Trader I want an Interest Ratings screen @view Scenario: View the Interest Ratings Given a repository of Interest Rating records When I view the Interest Rating screen Then the full list is created @create Scenario: Create an Interest Ratings record Given a repository of Interest Rating records When I create an Interest Rating with name Test And Interest Rating code 4 Then the Interest Rating is saved to the repository with name Test and code 4
The scenarios are the tests. The format is a clear Given-When-Then description. As you create the scenario the .feature.cs will be updated for you.
Now you need to link up the statements in your scenario to steps that the unit test framework can execute. Create a Steps folder under Features and add a SpecFlowStepDefinition. You’ll find the generated file has some useful placeholders to get you started. Here, for example, is InterestSteps.cs :
[Binding] public class InterestSteps { private IInterestRatingService interestService; private InterestRatingViewModel interestRatingViewModel; private InterestRating rating; private Mock<IValidationService> validationService = new Mock<IValidationService>(); private Mock<ILoadScreen> loadScreen = new Mock<ILoadScreen>(); private Mock<IServiceLocator> serviceLocator = new Mock<IServiceLocator>(); private Mock<IRepository<InterestRating>> interestRepository = new Mock<IRepository<InterestRating>>(); private List<InterestRating> interestRatings; [Given("a repository of Interest Rating records")] public void GivenARepositoryOfInterestRatingRecords() { Mock<IValidator> validator = new Mock<IValidator>(); this.serviceLocator .Setup(s => s.GetInstance<IValidationService>()) .Returns(this.validationService.Object); this.validationService .Setup(v => v.GetValidator( It.IsAny<InterestRating>())) .Returns(validator.Object); this.serviceLocator .Setup(s => s.GetInstance<IEventAggregator>()) .Returns(new EventAggregator()); this.serviceLocator .Setup(s => s.GetInstance<ILoadScreen>()) .Returns(this.loadScreen.Object); ServiceLocator.SetLocatorProvider(() => this.serviceLocator.Object); this.interestRatings = InterestRatingMother .CreateGoodInterestRatingMother() .InterestRatings .Cast<InterestRating>().ToList(); this.interestRepository .Setup(s => s.GetAll()) .Returns(this.interestRatings); this.interestService = new InterestRatingService( this.interestRepository.Object); } [When("I view the Interest Rating screen")] public void WhenIViewTheInterestRatingScreen() { this.interestRatingViewModel = new InterestRatingViewModel(this.interestService); this.interestRatingViewModel.Load(); } [When("I create an Interest Rating with name (.*)")] public void WhenICreateAnInterestRatingWithName(string name) { this.interestRatingViewModel = new InterestRatingViewModel(this.interestService); this.interestRatingViewModel.Load(); this.interestRatingViewModel.Add(); this.rating = this.interestRatingViewModel .InterestRatings[ this.interestRatingViewModel .InterestRatings.Count - 1]; this.rating.InterestRatingName = name; } [When("Interest Rating code (.*)")] public void AndInterestRatingCode(int code) { this.rating.InterestRatingCode = code; this.interestRatingViewModel.Save(); } [Then("the full list is created")] public void ThenTheFullListIsCreated() { Assert.That( this.interestRatings.Count == this.interestRatingViewModel .InterestRatings.Count); } [Then("the Interest Rating is saved to the repository with name (.*) and code (.*)")] public void ThenTheInterestRatingIsSavedToTheRepository( string name, int code) { InterestRating rating = (from m in this.interestRatingViewModel.InterestRatings where m.InterestRatingName.Equals(name) select m).Single(); Assert.That( rating.InterestRatingName.Equals(name), "The interest rating name was not saved."); Assert.That( rating.InterestRatingCode == code, "The interest rating code was not saved."); } }
In particular, notice the reuse of steps, for example GivenARepositoryOfInterestRatingRecords(), and the use of variable placeholders like (.*) to allow the passing of variables into the tests.
BDD wraps TDD. A reasonable flow would be to start with the Story, write up the Acceptance Tests and sketch out some of the steps. As you sketch out the steps you can see what unit tests you need so you go and develop the code using TDD. Once your code is ready and all the unit tests are passing you can integrate the layers with the BDD tests and when those are passing you have fulfilled your Acceptance Test.
Gherkin parsers for SpecFlow are on the way as are VS.NET language plugins (Cuke4VS – currently this crashes my VS.NET 2008).
Cuke4Nuke is another Cucumber port that is worth looking at.
The readability of the Features makes it easy to take Acceptance Tests from User Stories so that the Product Owner and Stakeholders can see what the system is doing. The “outside in” nature of the creating the code gives focus to fulfilling the User Story.