Click here to Skip to main content
15,867,308 members
Articles / Programming Languages / C#

Behavior-Driven Development with NBehave

Rate me:
Please Sign up or sign in to vote.
4.97/5 (35 votes)
13 Jan 2009CPOL11 min read 88.2K   376   81   17
A BDD tutorial using NBehave and MbUnit.

Contents

Introduction

This article is a pithy introduction to the concept of Behavior-Driven Development. In it, I present the way that the C# model of a bank account, with its trademark Deposit() and Withdraw() methods, can be created using BDD techniques. To do this, I will use the NBehave framework for BDD, and MbUnit as the underlying test framework.

Please note that for this introductory article, I'm avoiding the issue of mock objects – something that shouldn't be avoided in real projects, of course. Also, I will not talk about the issue of code coverage which is, of course, also important if you want to get your tests to cover all possible scenarios.

What's BDD?

Okay, you know what TDD is, right? TDD, also known as Test-Driven Development, is a very simple concept. In TDD, you write a Unit Test for some not-yet-implemented functionality, see it fail, add the necessary functionality, then see it succeed. Using TDD, your Unit Test for a withdrawal from a bank account might look like this:

C#
[Test]
public void WithdrawalTest()
{
  Account a = new Account(100);
  a.Withdraw(30);
  Assert.AreEqual(a.Balance, 70);
  Assert.Throws<InsufficientFundsException>(a.Withdraw(100));
}

This Unit Test is fine, but it tells you very little about what you are actually testing. What's essentially happening above is a comparison of states. For example, after the first withdrawal, you're comparing the Balance state of the account with the value 70. There's no notion of “the balance being reduced by the amount withdrawn” here. This test is very mechanical, and is not particularly descriptive.

Enter the notion of BDD. Basically, BDD is designed around the idea that, instead of describing the code under test as some sort of final state machine, you actually give it qualities related to its behavior. What this means is that you describe, in English, what each step in the Unit Test does, and associate the particular code with this step. You also provide additional metadata about the user story, i.e., what the particular user would want to happen in this test, and why it's important for them.

Sounds confusing? We'll take a look at a practical example in a moment, but before that, let's briefly discuss the libraries you need to add BDD to your application.

Tools of the Trade

First of all, you need NBehave, the library that actually allows BDD in the first place. Although the NBehave homepage is here, you should get the latest version from their Google Code repository. This is essential, because only version 0.4 contains the support for MbUnit that we need. If you get an earlier version, you won't be able to get MbUnit and NBehave to play together.

NBehave runs on top of a ‘conventional' Unit Testing framework, and I'm going to use MbUnit for this article. You can get MbUnit here, as part of the Gallio automation framework. The download link is right on the front page, so I'd just go for that.

As with conventional Unit Testing, you need some sort of test runner to actually run the tests and report results. If you have ReSharper, you've got nothing to worry about, since Gallio comes with a ReSharper plug-in for running all sorts of Units Tests, MbUnit included. If you haven't got ReSharper, you can use Gallio itself as it comes with its own test runner. All you have to do is open the assembly containing your Unit Tests, and you're set.

Example: Bank Account

Let's define an entity that models a bank account. It will have the following features:

  • A Balance property.
  • A Withdraw(int amount) method for withdrawing money from the account. If the amount of money is insufficient, we'll throw an InsufficientFundsException.
  • A Deposit(int amount) method for depositing money to the account.
  • A static Transfer(Account from, Account to, int amount) method for transferring money from one account to another.

For the above methods, let us also agree to throw an ArgumentException if the amount passed to any function is non-positive.

This very simple model is enough for us to get started with. In fact, I can code the interface for this class right now, since we need it anyway (without an interface, we cannot write tests – even failing ones).

C#
public sealed class Account
{
  private int balance;

  public int Balance
  {
    get { return balance; }
    set { balance = value; }
  }

  public void Deposit(int amount)
  {
  }

  public void Withdraw(int amount)
  {
  }

  public static void Transfer(Account from, Account to, int amount)
  {
  }
}

For the sake of completeness, here is the InsufficientFundsException class:

C#
public class InsufficientFundsException : Exception
{
  public InsufficientFundsException(int requested, int available)
  {
    AmountAvailable = available;
    AmountRequested = requested;
  }

  public int AmountRequested { get; set; }

  public int AmountAvailable { get; set; }
}

With the model in place, we can finally get started with BDD! Hooray!

Tests

Overview

Let's get started by adding the references to the project:

First, add a reference to the MbUnit assembly. It's in the GAC, so no searching is required. We only need the MbUnit.Framework namespace from this assembly for some essential attributes, such as [SetUp] – everything else is handled directly by NBehave.

We need three assemblies from NBehave – these are NBehave.Narrator.Framework, NBehave.Spec.Framework, and NBehave.Spec.MbUnit. The first of these assemblies imports the API for the so-called Narrator – an interface which mimics a narrator telling the user story. Basically, you'll be using the code to say things like “as a user, I want account withdrawal to work properly”. The second framework contains the base definition of a SpecBase class – this class is important because our test classes will need to derive from it. Finally, the third framework is – you've guessed it – a bridge between MbUnit and NBehave. In actual fact, this framework relies on NBehave.Spec.Framework, in that it contains a specialization of SpecBase for MbUnit.

One thing to note about NBehave.Spec.MbUnit is that, in addition to the SpecBase class, which we simply subclass and forget about (for the most part), this assembly also contains extension methods that mirror, to some extent, the Assert functionality of MbUnit. Here's what I mean:

C#
// in MbUnit, we write this
Assert.AreEqual(a.Balance, 70);
// but in NBehave, we write this
a.Balance.ShouldEqual(70);

The above statements are equivalent, but NBehave's version is perhaps more clear in expressing what it actually means. Please note, however, that at the moment, the extension methods which enable this behavior do not, at the time of this writing, cover every single functionality of MbUnit's Assert class. Thus, however nice this syntax is, you won't always be able to use it – particularly, if you rely on the more advanced functionality of MbUnit.

The Test Class

To cut the story short, here is the skeleton of my test class:

C#
[
  Author("Dmitri", "dmitrinesteruk@gmail.com"),
  Wrote("Account operation tests"),
  TestsOn(typeof(Account)),
  For("Banking system")
]
public class AccountTest : SpecBase
{
  public Account account;
  public Account account2;

  [SetUp]
  public void Initialize_before_each_test()
  {
    account = new Account();
    account2 = new Account { Balance = 100 };
  }
}

There isn't anything interesting happening here. Our test class derives from SpecBase, and is decorated with fairly standard MbUnit test attributes that have been redefined (see the next section) to make them more readable. In the test class itself, I create two accounts – one empty, and one with $100 on it. You'll notice that although I'm using the old-fashioned [SetUp] attribute, the method name is a bit strange. In fact, it's one of TDD conventions to have method names that actually describe what's going on, in English rather than some shorthand notation. So, that's precisely what's being done here.

Stories

It all begins with a story. Once upon a time, NBehave developers wrote the (somewhat clever) class they called Story. This class was designed to describe a particular set of usage scenarios which you are trying to test. For example, the story would describe the Deposit action on the account as follows:

C#
[Story, That, Should("Increase account balance when money is deposited")]
public void Deposit_should_increase_account_balance()
{
  Story story = new Story("Deposit");
  story.AsA("User")
    .IWant("The bank account balance to increase by the amount deposited")
    .SoThat("I can deposit money");

  // scenarios here
}

What's going on here, then? First of all, the attributes that decorate the test method are really using the familiar xUnit testing attributes (e.g., [Test]), but for BDD, many people (myself included) redefine them using C#'s using syntax:

C#
using That = MbUnit.Framework.TestAttribute;
using Describe = MbUnit.Framework.CategoryAttribute;
using For = MbUnit.Framework.CategoryAttribute;
using Wrote = MbUnit.Framework.DescriptionAttribute;
using Should = MbUnit.Framework.DescriptionAttribute;

The only reason for these redefinitions is to make the tests more readable. This trend permeates NBehave – get used to it. Now, let's take a look at what we did with the Story class. There is no Unit Test here! All we're doing is describing a user story, using English and NBehave's fluent interface. As you'll see later, most of NBehave uses a fluent interface, i.e., an interface where each function returns this, allowing long chains of calls to be made.

Just to be completely clear, we wrote the above story definition in order to reflect a particular requirement. For example, the requirements state that a user deposits money and their bank account grows accordingly, so we wrote just that. This story definition will appear in the output of our Unit Tests, making it somewhat easier to identify what it is we are testing.

Scenarios

We've created a story definition, so let's write a Unit Test and see it fail. In order to test it, we have to provide something called a scenario – a description of one possible thing that can happen. For example, you might withdraw from an empty bank account, or one that doesn't have enough money. Or you might withdraw an amount you actually have. Or you might try to withdraw a negative amount. All these cases are scenarios, and to get 100% coverage, you would need to test each one. However, let us start with something simple:

C#
story.WithScenario("Money deposit")
  .Given("My bank account is empty", () => { account.Balance = 0; })
  .When("I deposit 100 units", () => account.Deposit(100))
  .Then("The account balance should be 100", () => account.Balance.ShouldEqual(100));

All right, so this snippet probably exposes 99% of what BDD is about. Essentially, we're defining a scenario (a money deposit) and then describing the preconditions, the test itself, and the post-conditions – all in one C# statement! Here's what happens in our code:

  • We start with a WithScenario() call which tells the system what scenario this is. You've guessed it – the information does get output to the test tool! So does everything that follows it, mind you.
  • Then, we use the Given() method to define a precondition – i.e., an initially empty bank account. This function – just like the next two – is used with two parameters here: a string describing what's going on, plus an Action parameter which does what the previous parameter describes.
  • The When() is used to describe the action whose consequences we're trying to test.
  • Finally, the Then() call is where we check that the correct thing happened.

You have probably noticed by now that there's a fair bit of lambda syntax in the test. This is because the parameters in each of the clauses are of Action variety, so using a lambda syntax is somewhat more concise than using the delegate keyword.

Since we're doing BDD here, let's run the test to see it fail. On my system, I get the following error message:

*** DebugTrace ***
Story: Deposit

Narrative:
    As a User
    I want The bank account balance to increase by the amount deposited
    So that I can deposit money

    Scenario 1: Money deposit
        Given My bank account is empty
        When I deposit 100 units
        Then The account balance should be 100 - FAILED


MbUnit.Core.Exceptions.NotEqualAssertionException:
  Equal assertion failed: [[0]]!=[[100]]

Can you see how the test runner took the specification we wrote and actually output it as a readable scenario? It also shows us the point where it fails, so we don't have to decipher cryptic MbUnit messages (they are still available, so if you feel like it, be my guest). So, now that we have a failing test, let's add the missing functionality and try again:

C#
public void Deposit(int amount)
{
  balance += amount;
}

Simple enough. Now, when we run the test, it succeeds. That's all there is to TDD/BDD, really! But, let's look at a more complex scenario – depositing a negative amount. We should get an exception, and our bank account balance should remain unchanged. Here's how such a test would look:

C#
story.WithScenario("Negative amount deposit")
  .Given("My bank account is empty", () => { account.Balance = 0; })
  .When("I try to deposit a negative amount", () => { })
  .Then("I get an exception",
        () => typeof(Exception).ShouldBeThrownBy(() => account.Deposit(-100)))
  .And("My bank account balance is unchanged",
       () => account.Balance.ShouldEqual(0));

There are two things to note here. First, we use the ShouldBeThrownBy() extension method to ensure that when calling Deposit() with a negative amount, we do, in fact, get an exception. Also, we use the And() method to make sure that, in addition to the exception being called, the account balance remains unchanged. Running the test on our code, we get the following output:

*** DebugTrace ***
Story: Deposit

Narrative:
    As a User
    I want The bank account balance to increase by the amount deposited
    So that I can deposit money

    Scenario 1: Money deposit
        Given My bank account is empty
        When I deposit 100 units
        Then The account balance should be 100

    Scenario 2: Negative amount deposit
        Given My bank account is empty
        When I try to deposit a negative amount
        Then I get an exception - FAILED

The output is more or less expected, but we've had to break the paradigm somewhat in order to get the output we have here. Let me explain. First of all, I failed to provide an Action for when I was expected to actually deposit the amount. Instead, I used an empty lambda:

.When("I try to deposit a negative amount", () => { })

Basically, I cannot attempt a negative deposit in this clause, because I also intend to catch the exception and check that it is of the type I'm expecting – something more suitable to a Then() clause. On the other hand, I don't want to leave the When() clause action-less because if I do, the textual output (i.e., the “I try…” first parameter) will not appear in the output. This may be a bug or a feature, but in any case, I use an empty lambda to make sure that doesn't happen.

At this stage, I can simply finish off my naive implementation of the Deposit() function and run the test again. It would look something like this:

C#
public void Deposit(int amount)
{
  if (amount <= 0)
    throw new Exception();
  balance += amount;
}

Here's a situation where you cannot use the nice extension methods. Suppose you are testing a transfer between one bank account and another. You want to make sure that, if the transfer is possible (i.e., there is sufficient amount of money, it's a non-negative amount, etc.), that no exception should be thrown when the transfer happens. Since there is no ShouldNotThrow() extension method, we end up writing the following:

C#
story.WithScenario("Valid transfer")
  .Given("I have 100 dollars", () => { account.Balance = 100; })
  .And("You have 100 dollars", () => { account2.Balance = 100; })
  .When("I give you 50 dollars",
        () => Assert.DoesNotThrow(() => Account.Transfer(account, account2, 50)))
  .Then("I have 50 dollars left", () => account.Balance.ShouldEqual(50))
  .And("You have 150 dollars", () => account2.Balance.ShouldEqual(150));

Conclusion

By now, you've probably figured out that, apart from the way things are described (fluent interfaces, English descriptions), there is nothing new in NBehave. In fact, some people are annoyed that BDD is essentially a kind of 'verbose xUnit' that does the same things, but insists on describing everything you do. However, the benefit you get from this is traceability. For example, the words in your Unit Tests can refer to Use Cases in your requirements specification, thus making it easier to show that your product conforms to a particular spec. In fact, it's probably feasible to write a transformation tool that takes an English sentence and turns it into a skeleton NBehave scenario. If you write such a tool, please let me know!

This is it for this article. Thanks for reading. Comments and suggestions are much appreciated!

License

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


Written By
Founder ActiveMesa
United Kingdom United Kingdom
I work primarily with the .NET technology stack, and specialize in accelerated code production via code generation (static or dynamic), aspect-oriented programming, MDA, domain-specific languages and anything else that gets products out the door faster. My languages of choice are C# and C++, though I'm open to suggestions.

Comments and Discussions

 
QuestionStory attribute and Story Object Creation gives Compilation Error Pin
RohitVaidya3-Jul-17 22:56
RohitVaidya3-Jul-17 22:56 
GeneralMy vote of 4 Pin
gt.guybrush12-Sep-13 21:46
gt.guybrush12-Sep-13 21:46 
GeneralMy vote of 5 Pin
Kanasz Robert28-Sep-12 5:49
professionalKanasz Robert28-Sep-12 5:49 
GeneralNice One Pin
Muigai Mwaura6-Oct-09 23:05
Muigai Mwaura6-Oct-09 23:05 
Cheers
GeneralGreat Pin
NinjaCross7-Apr-09 23:54
NinjaCross7-Apr-09 23:54 
GeneralVery cool indeed Pin
Sacha Barber26-Jan-09 1:04
Sacha Barber26-Jan-09 1:04 
GeneralTake 5! Pin
6opuc26-Jan-09 1:01
6opuc26-Jan-09 1:01 
GeneralTestDriven.NET Pin
Steffen Buhr15-Jan-09 8:02
Steffen Buhr15-Jan-09 8:02 
GeneralRe: TestDriven.NET Pin
Dmitri Nеstеruk15-Jan-09 22:31
Dmitri Nеstеruk15-Jan-09 22:31 
GeneralRe: TestDriven.NET Pin
Morgan Persson7-Jul-09 9:08
Morgan Persson7-Jul-09 9:08 
GeneralLooking cool, but... Pin
Ishitori14-Jan-09 4:44
Ishitori14-Jan-09 4:44 
GeneralRe: Looking cool, but... Pin
Dmitri Nеstеruk15-Jan-09 22:28
Dmitri Nеstеruk15-Jan-09 22:28 
GeneralRe: Looking cool, but... Pin
Morgan Persson7-Jul-09 9:14
Morgan Persson7-Jul-09 9:14 
GeneralGreat article Pin
User 665814-Jan-09 3:19
User 665814-Jan-09 3:19 
AnswerRe: Great article Pin
Dmitri Nеstеruk15-Jan-09 22:43
Dmitri Nеstеruk15-Jan-09 22:43 
GeneralRe: Great article Pin
Morgan Persson7-Jul-09 9:21
Morgan Persson7-Jul-09 9:21 
GeneralExcellent Article PinPopular
TK_One14-Jan-09 0:16
TK_One14-Jan-09 0:16 

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

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