Table of Contents
I have been doing TDD (Test Driven Development/Design) for quite some now, and I have found this to be pretty useful. Writing test is always kind of pain if you don't get it right. The problem with TDD seems it's more oriented towards writing tests whereas the developer may be more concerned with the design and behaviour of the system.
The problem per se doesn't lie with TDD, but more with the mindset. It's about getting our mind in the right spot. So, here comes BDD a.k.a. Behaviour Driven Development. These are not just terminology changes, but it's about a change in the way we write our tests or rather specification in terms of BDD. Without further ado, let's dig deeper into this unchartered territory.
First things first. I don't wish to replicate things which are already published. So, the best place to get to know some theory is the Wiki: http://en.wikipedia.org/wiki/Behavior_Driven_Development [^].
But for the sake of completeness, here is a short summary. BDD is an agile software development technique that encourages collaboration between developers, QA, and non-technical or business participants in a software project. It's more about business specifications than about tests. You write a specification for a story and verify whether the specs work as expected. The main features of BDD development are outlined below:
- A testable story (it should be the smallest unit that fits in an iteration)
- The title should describe an activity
- The narrative should include a role, a feature, and a benefit
- The scenario title should say what's different
- The scenario should be described in terms of Givens, Events, and Outcomes
- The givens should define all of, and no more than, the required context
- The event should describe the feature
Check the References section 'An interesting read', for a more detailed explanation of each of the above points. We will be using the Membership Provider that comes with the default ASP.NET 2 MVC application (ASP.NET 1.0 MVC should also work) to write our stories that will revolve around "Registering a new user" for the site.
But before that, let's have a quick look at the tools that we will be using for this sample story.
The following are the list of tools that I will be using for this demonstration. Please set these tools up before proceeding or trying out. The download is self-contained will all the dependencies. But to get the code template for BDD, you have to install SpecFlow.
- SpecFlow
SpecFlow is the framework that supports BDD style specifications for .NET.
- NUnit
We will be using the classic NUnit for writing our unit tests.
- Moq
Moq is an excellent mocking framework for .NET.
SpecFlow is a BDD library/framework for .NET that adds capabilities that are similar to Cucumber. It allows to write specification in human readable Gherkin format. For more info about Gherkin, refer Gherkin project.
Gherkin is the language that Cucumber understands. It is a Business Readable, Domain Specific Language that lets you describe software behaviour without detailing with how that behaviour is implemented. It's simply a DSL for describing the required functionality for a given system. This functionality is broken down by feature, and each feature has a number of scenarios. A scenario is made up of three steps: GIVEN, WHEN, and THEN (which seems to be somewhat related to the AAA (Arrange, Act, Assert) syntax of TDD.
For more about Gherkin, refer to the Gherkin project.
- Download and run the SpecFlow installer.
- Create a new Class Library Project and add references to SpecFlow, Moq, and NUnit Framework.
We have named the Class Library Project as "SpecFlowDemo.Specs" to set the mindset that we are writing specifications for our business features.
Our intent is to get this nicely formatted report of our specifications:
Let's have a quick look at the UI for this.
The first step is to add a "SpecFlow" feature. We will keep all our features within the Feature folder in the Spec project that is created above. The feature file is where we're going to define our specifications. It's a simple text file with a custom designer which generates the plumbing spec code.
Let's add a new feature. Right click on the "Features" folder and "Add New" Item, and select "SpecFlowFeature" as shown below:
This creates a new file "RegisterUser.feature" and a designer file "RegisterUser.feature.cs". The default content of this file is shown below. This is in the Gherkin format.
Feature: Addition
In order to avoid silly mistakes
As a math idiot
I want to be told the sum of two numbers
@mytag
Scenario: Add two numbers
Given I have entered 50 into the calculator
And I have entered 70 into the calculator
When I press add
Then the result should be 120 on the screen
The "RegisterUser.feature.cs" file has the plumbing code to automate spec creation using NUnit (in this case). This file should not be manually edited.
The above template says what we we are trying to do, in this case, "Addition", and then there are different scenarios to support the feature.
Every time you save this file, you are invoking a custom tool "SpecFlowSingleFileGenerator". What it does is parse the above file and create the designer file based on the selected unit test framework.
Rather than explaining the above feature, let's dive into our first test case for "Registering a new user".
Feature: Register a new User
In order to register a new User
As member of the site
So that they can log in to the site and use its features
We have outlined our basic requirement in the above feature. Let's have a look at different scenarios that the application may have to deal with, with respect to the above feature.
Type/copy the below scenario to the .feature file.
Scenario: Browse Register page
When the user goes to the register user screen
Then the register user view should be displayed
Compile the spec project. Start up the NUnit GUI and open "SpecFlowDemo.Specs.dll". The first thing, change the following settings from Tools->Settings->Test Loader->Assembly Reload.
Ensure the following choices are checked:
- Reload before each test run
- Reload when the test assembly changes
- Re-run the last tests run
Doing this will automatically execute your tests whenever you compile them.
Now when you execute this test, you should be presented with the following screen. Click on the "Text Ouput" tab in the NUnit GUI.
You can see two "StepDefinitions" which the SpecFlow generated based on the "Scenario" specified in the feature file.
Now in Visual Studio->Your Spec Project->Add a New Class File. In our case, the name is "RegisterUserSteps.cs". This will be your spec class.
Copy the two methods to this file and delete out the line "ScenarioContext.Current.Pendin()
".
The full source code to our first scenario is shown below. There will be a couple of times I will be showing the full source code for easy understanding, and the rest of the times, I will only show the essential code snippet:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using TechTalk.SpecFlow;
using SpecFlowDemo.Controllers;
using SpecFlowDemo.Models;
using NUnit.Framework;
using System.Web.Mvc;
using Moq;
using System.Web.Security;
namespace SpecFlowDemo.Specs
{
[Binding]
public class RegisterUserSteps
{
ActionResult result;
AccountController controller;
[When(@"the user goes to the register user screen")]
public void WhenTheUserGoesToTheRegisterUserScreen()
{
controller = new AccountController();
result = controller.Register();
}
[Then(@"the register user view should be displayed")]
public void ThenTheRegisterUserViewShouldBeDisplayed()
{
Assert.IsInstanceOf<viewresult>(result);
Assert.IsEmpty(((ViewResult)result).ViewName);
Assert.AreEqual("Register",
controller.ViewData["Title"],
"Page title is wrong");
}
}
}
Compile the test and run it in NUnit. You will be presented with the below failure:
The reason for the error is, the "Register
" method uses the MembershipService
which we need to mock out. Have a look at the Register
method of AccountController
:
public ActionResult Register()
{
ViewData["Title"] = "Register";
ViewData["PasswordLength"] = MembershipService.MinPasswordLength;
return View();
}
To make this work, we need to add an overloaded constructor in the AccountController
which will take in the required dependencies.
public AccountController(IFormsAuthenticationService formsService,
IMembershipService memberService)
{
FormsService = formsService;
MembershipService = memberService;
}
Here' is the modified test along with the Moq objects:
[Binding]
public class RegisterUserSteps
{
ActionResult result;
AccountController controller;
Mock<imembershipservice> memberService = new Mock<imembershipservice>();
Mock<iformsauthenticationservice> formsService =
new Mock<iformsauthenticationservice>();
[When(@"the user goes to the register user screen")]
public void WhenTheUserGoesToTheRegisterUserScreen()
{
controller = new AccountController(formsService.Object, memberService.Object);
result = controller.Register();
}
[Then(@"the register user view should be displayed")]
public void ThenTheRegisterUserViewShouldBeDisplayed()
{
Assert.IsInstanceOf<viewresult>(result);
Assert.IsEmpty(((ViewResult)result).ViewName);
Assert.AreEqual("Register",
controller.ViewData["Title"],
"Page title is wrong");
}
}
Note in the above test we don't have the "Given" criteria. This is optional though. We have mocked Membership
and FormsAuthenticationService
and passed to the AccountController
. Here is the result of the test:
Now we have a passing test.
Scenario: On Successful registration the user should be redirected to Home Page
Given The user has entered all the information
When He Clicks on Register button
Then He should be redirected to the home page
The test code is described below:
[Given(@"The user has entered all the information")]
public void GivenTheUserHasEnteredAllTheInformation()
{
registerModel = new RegisterModel
{
UserName = "user" + new Random(1000).NextDouble().ToString(),
Email = "test@dummy.com",
Password = "test123",
ConfirmPassword = "test123"
};
controller = new AccountController(formsService.Object, memberService.Object);
}
[When(@"He Clicks on Register button")]
public void WhenHeClicksOnRegisterButton()
{
result = controller.Register(registerModel);
}
[Then(@"He should be redirected to the home page")]
public void ThenHeShouldBeRedirectedToTheHomePage()
{
var expected = "Index";
Assert.IsNotNull(result);
Assert.IsInstanceOf<redirecttorouteresult>(result);
var tresults = result as RedirectToRouteResult;
Assert.AreEqual(expected, tresults.RouteValues["action"]);
}
Scenario: Register should return error if username is missing
Given The user has not entered the username
When click on Register
Then He should be shown the error message "Username is required"
The test code is described below:
[Given(@"The user has not entered the username")]
public void GivenTheUserHasNotEnteredTheUsername()
{
registerModel = new RegisterModel
{
UserName = string.Empty,
Email = "test@dummy.com",
Password = "test123",
ConfirmPassword = "test123"
};
controller = new AccountController(formsService.Object,
memberService.Object);
}
[When(@"click on Register")]
public void WhenClickOnRegister()
{
result = controller.Register(registerModel);
}
[Then(@"He should be shown the error message ""(.*)""")]
public void ThenHeShouldBeShownTheErrorMessageUsernameIsRequired(string errorMessage)
{
Assert.IsNotNull(result);
Assert.IsInstanceOf(result);
Assert.IsTrue(controller.ViewData.ModelState.ContainsKey("username"));
Assert.AreEqual(errorMessage,
controller.ViewData.ModelState["username"].Errors[0].ErrorMessage);
}
Some points of interest in this test case: notice the (.*) expression in the "Then" part. This allows you to pass parameters to the test. In this case, the parameter is "Username is required", which is passed form the "feature" file.
Please go through the source to find the complete set of test cases. Hope I was able to touch base on these excellent topics.
As a final note, to get the *HTML* output, do the following steps as shown in the figure below:
Open NUnit GUI and go to Tools->Save results as XML. Give a name to the file and save it in your project location. Then, do the following external tool setting for the VS IDE:
- Go to Tools->External Tools menu in Visual Studio
- Click on Add
- Fill in the values as shown in the figure above
To get the *HTML* report, click on "SpecFlow" from the Tools menu.
Here is the location of my NUnit XML file:
Hope you find this useful. I will update this with more improvements and test cases. For the basics of TDD and BDD, there are numerous articles on CodeProject for reference.
- May 21, 2010 - First published.