Click here to Skip to main content
Click here to Skip to main content

BDD using SpecFlow on ASP.NET MVC Application

, 20 May 2010
Rate this:
Please Sign up or sign in to vote.
I will give a walkthrough of the story of registering a new user using BDD on an ASP.NET MVC application.

Table of Contents

Introduction

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.

What is BDD?

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.

Essential tools for BDD

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.

Overview of SpecFlow

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.

Initial setup

  1. Download and run the SpecFlow installer.
  2. 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.

The result at a glance

Our intent is to get this nicely formatted report of our specifications:

Story: Register a new user

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".

The feature: Register 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.

Scenario 1 - Browse register page

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 2 - On successful registration, the user should be redirected to Home page

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 3 - Register should return error if username is missing

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.

References

History

  • May 21, 2010 - First published.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

Rajesh Pillai
Architect
India India
Co Founder at TekAcademy Labs and Principal Consultant and Chief Architect as Megasoft Informations Systems Pvt. Ltd.
 
http://tekacademy.com/
Follow on   Twitter

Comments and Discussions

 
GeneralMy vote of 5 Pinmemberazamsharp11-Feb-11 6:26 
I do not usually comments on articles but this article is simply outstanding! Awesome work!
 
I do have one question! Why does the following line returns String.Empty even though I do have a view:
 
Assert.IsEmpty(((ViewResult)result).ViewName);
GeneralRe: My vote of 5 PinmemberRajesh Pillai19-Feb-11 16:46 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Mobile
Web04 | 2.8.140827.1 | Last Updated 21 May 2010
Article Copyright 2010 by Rajesh Pillai
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid