devxlogo

Achieve the Best of Two Worlds with Behavior-Driven Development

Achieve the Best of Two Worlds with Behavior-Driven Development

uch of agile software development methodology is grounded in the lean manufacturing principles developed and perfected by the Toyota Motor Company, and translated to software production by a number of software luminaries in the 90’s.

Toyota understood that eliminating waste was essential to achieving its goals, and it understood that a rigorous pursuit of quality for its processes and products was essential to eliminating waste.

The rework caused by defects and the downstream difficulties brought about by components built earlier in the process are powerful inhibitors to throughput. Lean and agile approaches depend on frequent inspection and feedback of work product at every stage of the process to avoid creating waste. Rich and frequent communication along with the focus on quality is what allows the advances in productivity and flexibility achieved by agile organizations.

Scrum, XP, and the Communication Gap
Extreme Programming (XP) brought mainstream attention to agile development. XP borrowed planning practices from Scrum, but in the beginning, Scrum was a bit of an unknown compared to XP.

It wasn’t long before it became clear that using Scrum and XP together brought needed organization and planning practices together with agile engineering practices. Yet since XP and Scrum are separate methodologies, there are gaps between the two that sometimes make for a bumpy ride when transitioning from Scrum’s planning practices to XP’s software engineering practices.

When formulating acceptance criteria, any new criteria that surface in the process of elaborating on a user story in code are captured directly in code, and can be communicated back to the customer later if necessary.

XP and Scrum necessarily have two processes in play on agile teams addressing different aspects of software production. Behavior-Driven Development (BDD) comes out of the friction between these two processes. BDD tightens communication and feedback across the gap between XP and Scrum. It offers some new ways to use old practices?and these in turn bring more focused workflows to agile development that might be considered a methodology of its own.

An Old New Thing
Agile project teams use XP and Scrum together because each has a focus that the other doesn’t have; but XP and Scrum also have a lot of common ground and that makes them natural partners.

With a layer of Behavior-Driven Development over XP/Scrum, the individual agile practices tend to flow together with fewer coarse transitions between analysis, design, development, and testing activities.

Getting XP and Scrum to play well together involves being slightly stricter with some of the practices, as well as tweaking some of the practices subtly so that they complement each other better.

BDD creates more harmony between the user story practices from Scrum and the Test-Driven Development (TDD) practices from XP. The user stories practices represent analysis and specification in agile projects, while TDD represents software design.

The resulting increased fluidity between analysis and design is brought about by a stronger focus on acceptance criteria. Acceptance criteria are those parts of requirements that specify when a requirement has been satisfied by shippable software.

BDD borrows Domain-Driven Design’s focus on establishing and evolving a shared language (ubiquitous language in DDD parlance) used by the team in requirements, abstractions (class names, variable names, methods, database tables, columns, etc.), specifications, documentation, and all manner of project artifacts, as well as in conversations and other communications between members of the team, customers, and stakeholders.

BDD uses “Context/Specification” test style in its TDD practice to arrive at smaller, simpler, and more focused test groupings that more readily document the system. Context/Specification tests focus on how the system and its modules and units behave in specific usage contexts rather than on how individual classes and methods have been implemented.

The contextual perspective more naturally reflects the system’s user stories, and often leads to a more fluent understanding of the customer’s expectations of the system. Contextual specification helps keep developers focused on the desired user experience, and produces test code that is more navigable, more discoverable, and generally more understandable?which is itself a boon to agility, raising productivity and quality to a yet higher level.

It’s Still TDD
Nothing in Behavior-Driven Development changes Test-Driven Development’s mechanics. Everything that you do today with TDD is still done in exactly the same way. Not even the use of Context/Specification style tests changes the essential work style and goal of practicing of TDD. Red-Green-Refactor, test-first programming, friction-aversion, design principles and practices, micro-incremental design, and all the usual suspects are still in play.

It’s perfectly valid to continue to refer to TDD when talking about those parts of the practice that are specific to TDD mechanics. It’s not just because BDD is the new thing that developers immediately have to replace TDD with BDD in their vernacular?especially since the terms aren’t always synonymous. BDD covers more ground than TDD, although it has TDD at its core.

It’s About Design
Like Test-Driven Development, Behavior-Driven Development is concerned with software design. As with TDD, it leverages friction found in testing to indicate the presence of subtle design problems that will cause downstream software production throughput problems, causing the code and the system itself to be harder to work with each time new code is integrated into the whole.

TDD is often specifically about the design of code units and modules such as classes. BDD is also concerned with unit design, but its focus addresses a much broader range of design concerns.

BDD is used in support not only of unit design, but also in support of shared language design, test design, and even user experience design.

Editor’s Note: This article was first published in the May/June 2008 issue of CoDe Magazine, and is reprinted here by permission.

Client-Driven Development
Test-Driven Development could more accurately be called “Client-Driven Development.” The practice encourages unit design to be done starting from its API. Rather than conceive of an API design in some kind of modeling tool, Client-Driven Design practices force the designer (developer) to prove the value of the API’s design by experiencing it first. Client-Driven practices such as TDD act as a kind of user-experience testing for APIs, classes, and other modules.

If you’ve ever worked with an API that doesn’t seem to make any sense to you as a developer, then it’s quite possible that the API was designed using assumption-driven rather than test-driven development. All the best assumptions in the world might not amount to much when it comes down to using that API in real projects.

Often, APIs built using TDD are easier to work with, where APIs resulting from assumption-driven development may cause unnecessary friction, making it harder to achieve the level of feedback that drives lean development, and reduces the overall quality of the product and the development process.

An API is assumption-driven when it isn’t generated from examples of code that demonstrate how a developer would ideally use the API.

When you’re using client-driven development to build an API, you first write down examples that present the API in context with how it might be used, showing the API at its best based on how you would wish it to be if you were its designer?which you in fact are. So make the API you wish you had, without imposing any restrictions on your drive to make the API that brings ease to development. Then you can use any problems that occur while getting the example code to work to tell you that the design has flaws.

When this client code feels right, you generate the API code using a refactoring tool and incrementally fill in the implementation, continuing to run the example code as you go, immersed in the feedback that comes from seeing the code work or fail.

You could use any old VB or C# Visual Studio project to write down these snippets of client code, such as a console project, or a Windows Forms project, but it’s usually much easier just to use one of the readily-available unit testing tools like NUnit to capture these API examples and run them. Testing tools provide ways to select the code to run, they provide results of the execution of the examples through Windows user interfaces and IDE integration, and they provide APIs of their own to help determine that the examples do indeed work correctly.

If you design the API with TDD, you do so from the perspective of code that acts as a client to the API. This way, the designer (developer) puts himself in a position to be a user of an API, and then designs the user experience of the API so that it reflects his needs and his ideas of a good and sensible experience.

You wouldn’t think of designing any kind of software interface?user interface or other?from a position where you could not get immediate feedback from the client. If you did, you’d just be building software based on your assumptions of what the client wants, rather than putting him in front of the software and having him tell you if it feels right or wrong.

Assumption-Driven Development (ADD) is a frequent symptom of phased-based development processes, and processes that attempt to accomplish detailed API design using diagramming tools such as Visual Studio’s class designer and extremely abstract languages such as UML. ADD causes waste by reducing accuracy and increasing rework.

APIs and the classes, interfaces, and modules invariably end up reflecting more desirable software design qualities when designed from the client perspective. Client-Driven Design produces code that is more loosely coupled and more cohesive, and pays greater respect to design-first principles than typical ADD code.

Code that reflects first principles is easier to understand, easier to work with, and typically offers greater opportunities for reuse.

Folks who use TDD do so because it exercises the code’s design as the first act and first priority of implementation. Practitioners of TDD use testing frameworks when creating the client code because they provide code runners, which eliminate the need to write custom drivers for their client code.

A New Name
With all this talk about testing and bandying about the word test so much, folks got the idea that Test-Driven Development was all about testing.

Folks who do TDD for a while typically come to the conclusion that the only thing that TDD has to do with testing is the appearance of the word test in its name. That’s a bit of an exaggeration, but it’s a common one used by TDD’ers to help clarify the issue.

We use unit testing frameworks to do TDD, and we write the example code in methods that can be executed by the test runners that come with unit testing frameworks, but that’s about the end of the commonalities between TDD and testing.

If you don’t write your tests first in order to surface and weed out design friction and create a positive user experience for your API, you’re not really doing Test-Driven Development. You may have every .NET unit testing framework under the sun plugged into your solution, and you might write unit tests until the cows come home, but merely the presence of unit testing in your project isn’t an indicator that you’re engaged in client-driven practices like TDD in support of lean software production.

To be fair, the code that TDD practice leaves behind gets stored with the application code and can be used to verify the correct behavior of the system through its lifetime. So, you get testing out of TDD after all, but it comes largely as a side effect of doing Client-Driven Development armed with a unit testing framework.

Behavior-Driven
TDD’s essential goal is to design and specify the behaviors of the system according to the expectations of the customer, and to be able to consistently prove that those behaviors work according to those expectations for as long as the product lives.

In an effort to clear up the misconceptions that come from the name Test-Driven Development, and to shine some light on understandings that were surfacing from years of practices of TDD, Dan North coined the term Behavior-Driven Development to more accurately frame the practice.

Ultimately, anything that could be thought of as BDD could have been done with plain old TDD, and in fact, it often was?for folks who hadn’t been misled by the confusion that the word test had caused. The rest of us spun our wheels for years, struggling, as Dan North puts it, wanting to know where to start, what to test and what not to test, how much to test in one go, what to name tests, and how to understand why a test fails.

What systems do is of greater concern than the data that gets done to. Even when we’re writing data access components, we’re concerned with the behavior of the system rather than the data itself. A bug will happen because of problems with the system’s behavior. That bug might manifest itself as incorrect data, but it’s the system’s behavior that is causing the data to be incorrect.

Business objects are behavioral objects. They likely hold on to data, but the data is there to support the things that the business can do with it through the behaviors of the system. We need to get the behaviors right, and we need to describe them so that other developers who work with the code can gain an understanding of how things work and what things are. As developers, we do behavior specification of the system’s implementation using code, but before we get to the code, the customer is already specifying behavior via user stories.

User Stories
User stories are the central axis around which a software project rotates. Developers use user stories to capture requirements and to express customer expectations. User stories provide the unit of effort that project management uses to plan and to track progress. Estimations are made against user stories, and user stories are where software design begins. User stories help to shape a system’s usability and user experience.

User stories express requirements in terms of “The Role, The Goal, and The Motivation.”

A user story isn’t concerned with a named user, such as “Bob Smith” or “Anne Wilson.” User stories are concerned with a user role, for example, a customer, or a salesman, or a call center operator.

User stories describe what that user wants to do from the perspective of an interaction with a business process, such as purchasing, or ordering, or paying a bill. A user story describes the user’s goal in terms of needing to get something done with the business, rather than in terms of using the system itself.

A user story also captures the user’s motivation for wanting to accomplish the goal. The motivation captures some of the back-story and the context for the user’s interaction with the business. The back-story provides clues that can guide further analysis of the application with subject matter experts and business analysts.

A story’s motivation statement provides some insight into a user’s reasonable expectation for how the feature or function that satisfies the story may work. Without stating the motivation, a lot is left to interpretation by the implementers, and the customer may simply not have his expectations met. A missed expectation is waste and leads to rework.

A story’s motivation focuses the aim of the story, helps all the stories of the system fit together and fit with users’ expectations, and provides the extra context that helps us consider what a good user experience might be.

Without stating the user’s motivation for the story, the implementers simply may not have a clear understanding of what the customer was really trying to accomplish and how he needed to do so. A story without a statement of the user’s motivation is often thought to be unactionable?that is, the story can’t be easily estimated or implemented.

A story’s motivation can be optionally omitted from a story that isn’t going to be implemented immediately. If you place a story in the project backlog rather than the iteration backlog, then you might omit the motivation temporarily if the motivation isn’t clear to you. However, when the story moves into an iteration backlog, you should make all reasonable efforts to surface the user’s motivation.

Example User Story
You’ll usually write user stories according to the following template:

   As a  I want to  so that .

The following example user story reflects requirements for a funds transfer between two bank accounts. After having a discussion with the customer, the following requirement surfaced:

   As an account holder, I want to transfer funds   between two of my accounts.

To help provide more context to the story, you should discuss with users why they want to do a funds transfer. Although the reasons for doing a funds transfer seem self-evident, suppose that you discussed this with the user a few times to try and surface the motivation. Often, it’s these aspects of a story that seem so self evident that are ultimately not so evident at all when you try to express them.

Here’s the full story with the motivation expressed by the customer:

   As an account holder, I want to transfer funds   between two of my accounts so that I can maximize   the performance of my savings and avoid any fees   associated with overdrafts and minimum balance rules.

Now that you have the motivation, you have a more enriched understanding of the system’s context and the user’s concerns. This lets you operate in a knowledge-rich environment. Having a grasp of the user’s concerns helps you shape a good user experience and clarify the customer’s expectations.

So far you’ve identified the users as “account holders.” This differentiates them from any of a number of other user roles that might be able to transfer funds using this system. For example, a teller or a bank manager can probably execute a funds transfer, but the constraints on the behavior of the software might be different for those user roles.

The differentiation of roles gives you a mental framework for considering a user’s particular needs and assess whether his role and his particular motivation might require you to write specific code to address any unique needs.

It’s not necessary to write any hooks in the code for possible variations that the story might be hinting at. Test-Driven Development allows you greater reversibility of decisions, so it’s not necessary to overindulge in the kind of pre-emptive coding; you can just as easily deal with that later. In agile parlance, this is referred to as YAGNI, which stands for “You Ain’t Gonna Need It.”

The YAGNI guiding principle keeps you from adding hooks and extension points to the code until you have concrete, present requirements in the code directing you to do so, and pulling the code in that direction. YAGNI is a form of waste. In lean production terms, this form of waste is called over-processing.

User Stories Are Conversations
TDD developers think of user stories as placeholders for conversations that they purposefully keep terse. User stories aren’t exhaustive requirements specifications, and they’re not supposed to be.

Because user stories aren’t exhaustive, we can do requirements specification interactively and incrementally in concert with the customer throughout the project rather than in one big analysis activity as is done in phased waterfall-styled software production. User stories are a form of software requirement that help teams do the software development process in a just-in-time fashion.

User stories may remain somewhat undefined and abstract while they remain in the product backlog. As the project team chooses stories for implementation in the next iteration, they flesh out a story’s details. This is how detailed analysis and design get started in agile projects.

A team surfaces a story’s acceptance criteria though conversations with the customer and the architect or technical lead. Other members of the team may participate, but it’s not always efficient to do this kind of work in a large committee. Regardless, the rest of the team will have an opportunity to have their say during the iteration planning and estimation session.

During iteration planning, the customer and the development team sit down to discuss the stories and the customer’s expectations of the implementation. At this point even further design details can-and usually do-surface.

Based on a reasonable understanding of the design details and constraints, the development team estimates the amount of work they need to do to implement a user story. Sometimes the development team will discover that a user story is too big and must be broken up. Sometimes smaller stories are conjoined, and sometimes entirely new stories are surfaced.

All this is done in conversation with the customer. When the iteration planning session is done, the team has generated and communicated a tremendous amount of shared understanding about the stories and their expectations.

Still, there might be grey areas that surface while the team is turning the story into software. Subsequent ad hoc conversations between the customer and the development team ensue. And again, these conversations can include any number of members of the team or the whole team.

It’s important to note that because the members of an agile team all work together in a common space, many conversations happen out in the open and even folks not directly involved in a conversation might interject if necessary.

Requirements and design constraints are communicated and held through conversation and shared understanding. That doesn’t prevent architecture and modeling from taking place as a result of these conversations, nor does it inhibit generation of harder artifacts, but the starting point for these artifacts are user stories and the conversations they engender throughout the project.

Contexts and Specifications
Acceptance criteria are story specifications written in plain language, transformed into lower-level specifications written in code, and executed using a testing or specification framework.

After talking in greater detail with the customer about his expectations for the funds transfer, the following three acceptance criteria surfaced:

   The amount of the transfer is debited from the "from"   account and credited to the "to" account      The "from" and "to" accounts are different accounts      Cannot transfer an amount greater than the balance of the   "from" account

These acceptance criteria are the starting points for test-first programming. The development team can translate these criteria into specifications that in the team will write as code and implement using Test-Driven Development.

Members of the team will gather sets of related observations (tests) into contexts. Contexts aren’t simply arbitrary groupings; they are representations of cohesive and consistent circumstances that software modules can be found in. TDD teams use contexts to describe anything from system states to user experiences.

The examples that follow present context specifications as having concerns, contexts, and observations.

Consider the first acceptance criteria: The amount of the transfer is debited from the “from” account and credited to the “to” account. Here’s a first cut at representing the acceptance criteria as a description, based on a concern, a context, and observations:

   Concern: Funds Transfer   Context: When transfering between two accounts   Observation: The amount is debited from the "from" account   Observation: The amount is credited to the "to" account

Here’s a context-based description of the second acceptance criteria, “The ‘from’ and ‘to’ accounts are different accounts:”

   Concern: Funds Transfer   Context: When transfering between two accounts where the   accounts are the same account   Observation: The transfer is not allowed

This brings up the issue of the sameness or equality of two accounts. It suggests a need for a means to determine whether two accounts are the same.

After discussing this with the customer, it was determined that two accounts are the same account if they have the same account number. So the team added new acceptance criteria to the story.

   Accounts are equal if they have the same account number

Below you can see the derived context-based representation of the criteria:

   Concern: Account Equality   Context: When determining whether two accounts are the   same   Observation: The accounts are equal when they have the   same account number   Observation: The accounts are not equal when they have   different account numbers 

The last criteria is, Cannot transfer an amount greater than the balance of the “from” account. Here is the specification:

   Concern: Funds Transfer   Context: When the balance of the "from" account is less   than the amount of the transfer   Observation: The transfer is not allowed

It’s not necessary to go through the motions of reformatting acceptance criteria as contexts and specifications before getting down to the work of writing code.

Usually, the acceptance criteria are taken directly from the form written by the customer in the customer’s comfortable language, and translated directly into code by the developer who uses the customer’s specified criteria and any notes that he’s made in the conversation with the customer to get into any greater details.

It’s not even necessary to transcribe the acceptance criteria addressing account equality to the list of criteria that are formally associated with the user story. This level of specification might be deemed to be far too low-level for the story’s acceptance criteria.

You aren’t trying to achieve seamless traceability of requirements with user stories and acceptance criteria. You’re capturing just enough specification to get the work started.

Any new criteria that surface in the process of elaborating on a user story in code are captured directly in code, and can be communicated back to the customer later if necessary.

Executable Specifications
Sooner or later the conversations have to subside and the team has to write some software. As mentioned earlier, the team will do their coding using a client-driven approach like Test-Driven Development.

I didn’t write this article to rehash a decade of material on Test-Driven Development, but rather to demonstrate how to use the user stories and Test-Driven Development to forge tighter bonds and smoother transitions between user stories and conversations, and TDD through increased feedback and communication.

Test-Driven Development is a form of executable specification. Teams use TDD and test-first programming in Behavior-Driven Development to turn a story’s acceptance criteria into example code that specifies the application code that the story requires.

An Example
Starting with the first criteria, the amount of the transfer is debited from the “from” account and credited to the “to” account, here’s a skeleton of the specification written in code:

   [Concern("Funds transfer")]   public class when_transferring_between_two_accounts   {      [Observation]      public void          should_debit_the_from_account_by_the_amount_transferred()      {      }         [Observation]      public void         should_credit_the_to_account_by_the_amount_transferred()      {      }   }   

This class represents the specific context and observations that can be made about the specification.

It is also a standard NUnit test class and will be run by the NUnit test runners. Any assertions used in the implementation of the test methods will push information to the test runners and be included in NUnit output.

Note that this ability is in the current NUnit source, which hasn’t been released as an official build. You can build this version of NUnit from source, or you could borrow the NUnit binaries that are in the SpecUnit .NET source tree.

The ConcernAttribute on the class definition is a subclass of NUnit’s TestFixtureAttribute, and the ObservationAttribute is a subclass of NUnit’s TestAttribute.

These attributes are part of the SpecUnit .NET helpers for NUnit. You can find the source code and binaries on Google Code.

SpecUnit .NET has a tool that produces an HTML report of the specifications. The tool is executed against the assembly that contains the specifications. I’ve named the specification assembly Banking.Specs.dll. You can produce the report at the command line with:

?
Figure 1. Report Specifications: Here’s the report for the specifications so far.
   SpecUnit.Report.exe Banking.Specs.dll

Figure 1 shows the report for the specifications so far.

You should write only one specification or observation method at a time. I wrote two above just as an example.

Here are the implementations for the first two specifications:

   [Observation]   public void      should_debit_the_from_account_by_the_amount_transferred()   {      Account fromAccount = new Account { Balance = 1m };      Account toAccount = new Account { Balance = 1m };      // because      fromAccount.Transfer(1m, toAccount);      fromAccount.Balance.ShouldEqual(0m);   }         [Observation]   public void       should_credit_the_to_account_by_the_amount_transferred()   {      Account fromAccount = new Account { Balance = 1m };      Account toAccount = new Account { Balance = 1m };      // because      fromAccount.Transfer(1m, toAccount);      toAccount.Balance.ShouldEqual(2m);   }

These specification methods share some common code?and in this case, the common code isn’t really a representation of the observations themselves. That is, there are only two lines from the specification methods that are C# versions of the observations as expressed in the method names. They are:

   fromAccount.Balance.ShouldEqual(0m);

and:

   toAccount.Balance.ShouldEqual(2m);

The following code shows what the observations look like:

   [Observation]   public void       should_debit_the_from_account_by_the_amount_transferred()   {      _fromAccount.Balance.ShouldEqual(0m);   }   [Observation]   public void       should_credit_the_to_account_by_the_amount_transferred()   {      _toAccount.Balance.ShouldEqual(2m);   }

In both cases, the specification’s methods present the exact code required to make the observation?no more and no less. This is the ideal specification code. It leads to much-improved experiences in understanding the system and the code itself.

The code below shows what the entire context becomes when you move the common code out of the specification methods:

   [Concern("Funds transfer")]   public class when_transferring_between_two_accounts :       ContextSpecification   {      private Account _fromAccount;      private Account _toAccount;      protected override void Context()      {         _fromAccount = new Account { Balance = 1m };         _toAccount = new Account { Balance = 1m };      }      protected override void Because()      {         _fromAccount.Transfer(1m, _toAccount);      }      [Observation]      public void          should_debit_the_from_account_by_the_amount_transferred()      {         _fromAccount.Balance.ShouldEqual(0m);      }      [Observation]      public void          should_credit_the_to_account_by_the_amount_transferred()      {         _toAccount.Balance.ShouldEqual(2m);      }   }

The Context() method above provides the resources needed to represent the context. The Because() method is the precise line of code that causes the subsequent observations to be made.

You could include the code in the Because() method in the Context() method, but isolating it in its own method calls out the exact core behavior for the specification in no uncertain terms, providing clearer documentation of the intent of the concern. Using the Because() method is completely optional (as is using the ContextSpecification base class), and you could just as easily move its contents to the Context() method if you prefer. Sometimes, only using the Context() method is more sensible and desirable.

This class’s base class is ContextSpecification, which is included in the SpecUnit .NET binaries. Developers who use the Context/Specification style often create their own base class to provide more appropriate language to their specifications. You might find that you’d prefer to write your own.

Another Example
Implementing the specification that describes the constraint on account equality for funds transfer forces me to implement the equality constraints for the accounts. Rather than open that can of worms right off the bat, I’m going to choose to implement the Cannot transfer an amount greater than the balance of the “from” account specification:

   [Concern("Funds transfer")]   public class        when_transfering_an_amount_greater_than    _the_balance_of_the_from_account      : ContextSpecification   {      private Account _fromAccount;      private Account _toAccount;      protected override void Context()      {         _fromAccount = new Account { Balance = 1m };         _toAccount = new Account { Balance = 1m };      }      [Observation]      public void should_not_allow_the_transfer()      {         typeof(Exception).ShouldBeThrownBy(delegate         {            _fromAccount.Transfer(2m, _toAccount);         });      }   }

Note that I didn’t write this observation to say that the code should raise an error or throw an exception. That fact is secondary to the fact that the transfer is disallowed. If I want to know the implementation details of this constraint, I’ll read the implementation code of the specification rather than its description as captured by the observation text.

?
Figure 2. Inadequate Funds: Here’s the specification to deal with inadequate funds.

Figure 2 shows the updated specification report, including this new specification.

The preceding code example used the ShouldBeThrownBy() method in place of NUnit’s traditional ExpectedExceptionAttribute on the test method. The ExpectedExceptionAttribute causes the specification to be expressed in two locations for the same method: first, in the body of the method, and second, above the method declaration where the attribute is added.

The SpecUnit .NET binaries include a set of extension methods that allow for a more literate style of expressing expectations and assertions. The ShouldEqual() method used in the first example, and the ShouldBeThrownBy() method are both extension methods provided by SpecUnit .NET.

Reuse: Friend or Foe
The example contexts have some shared code. A programmer’s first instinct is likely to refactor this shared code into a common base class?but this is usually the last thing you want to do with specification code!

Specification code is intended to document the behaviors of the system, and this often means leaving duplicated code in place to support the “learnability” of the specs, and the readiness of the concepts and notions communicated by the specs.

If you do move common context code to a base class, do so with care, with full knowledge of how you’re impacting the learning experience of the code. People are often called upon to read he code written by others on their teams, or code written by people who have since moved on?especially on agile teams where collective ownership of the code by the whole team is the rule.

The goal is to create the best possible learning experience for people who are called upon to learn from your code and to understand it?possibly for the first time! Scattering a specification’s code across multiple context class definitions just for the sake of reuse isn’t considered good form in writing specification code (or plain old unit test code, for that matter).

?
Figure 3. Common Code: Here’s the common code represented in a specification.

Nonetheless, I’ve included an example (see Listing 1) that shows how to represent the common code as a common base class.

The previous example uses a base class to capture the shared code. I changed both specification classes to extend this class rather than the ContextSpecification class.

Figure 3 shows the specification report, updated to convey the shared context.

Again, use your best judgment when trying to achieve reuse in specification or test code. Reuse is often considered an anti-pattern in this context as opposed to in application code, where reuse is often considered desirable.

A Few Changes for Clarity
The exception-handling code in the second context class is not as clear as it could be. It would be better to get the code that represents the observation down to a single line so that the code maps more directly to the observation text. The following code makes this happen:

   [Concern("Funds transfer")]   public class      when_transfering_an_amount_greater_than    _the_balance_of_the_from_account     : behaves_like_context_with_from_account_and_to_account   {      private Exception _exception;      protected override void Because()      {         _exception = ((MethodThatThrows) delegate         {            _fromAccount.Transfer(2m, _toAccount);         })         .GetException();      }      [Observation]      public void should_not_allow_the_transfer()      {         _exception.ShouldNotBeNull();      }   }

In the preceding example moves the cause of the observation being made from the specification method into the Because() method. Depending on your preference, you might not find this any clearer?or you might even find it less clear.

With the specification in this form, you can add a clarifying observation about the exception, and communicate more appropriate information through the specification report:

   [Observation]   public void should_raise_System_Exception()   {      _exception.ShouldBe(typeof(Exception));   }
?
Figure 4. Clarifying Observation: The figure shows the specification after adding a clarifying observation about the exception.

Figure 4 shows the updated specification report.

Full Circle
With the specification report in hand, the development team can take a step back and check their thinking against the specifications and criteria given by the customer.

As the development team writes code that intends to deliver on a customer’s expectations, the team might add new implementation-level criteria that might best be vetted by the customer, architect, product designer, or another developer.

The specification report is also an ideal document to give to testers to describe in plain language the expected behaviors of the system. They can use the specification report to understand what the system does and what they need to test.

The report is a valuable design review tool as well. It has been a constant source of surprise (and pleasure) to me to see developers review the specification report for code that they have just written and find tactical design flaws and misinterpretations of requirements!

A good deal of the effectiveness of the specification report comes from using a Context/Specification style for writing tests. It would be quite difficult to surface this kind of documentation from traditional unit tests that don’t document individual contexts, but merely collect sets of seemingly unrelated tests for a given application class into a given test class.

The Context/Specification style has also proven to be an effective style for writing specifications that are easier to understand and can be understood much more quickly. This supports the navigability of the code base, brings yet more agility to the experience of the code, and eliminates yet more waste in the effort to develop, test, and maintain the system.

Whole Practice
The benefits of user story practices and Test-Driven Development practices have been key elements in agile software development using Scrum and XP. The entire concerted effort toward lean software production would be quite hampered if Behavior-Driven Development were merely practiced as a novel unit-testing pattern.

Agile development methodologies’ strength are in the concerted and orchestrated employment of its many practices, intended to complement and strengthen each other, and to strengthen and unify diverse and cross-functional teams.

You might choose to only use the Context/Specification style for writing unit tests, and you would inevitably benefit from it, but Behavior-Driven Development is a whole practice that has tremendous positive effects on a software production effort, and the software team when practiced as a whole.

Behavior-Driven Development, like XP and Scrum, is more than the sum of its parts.

Final Thought
I left the last remaining specification from the example as an exercise for the reader. If you take a stab at it and post it on your blog or elsewhere, please send me a link.

devxblackblue

About Our Editorial Process

At DevX, we’re dedicated to tech entrepreneurship. Our team closely follows industry shifts, new products, AI breakthroughs, technology trends, and funding announcements. Articles undergo thorough editing to ensure accuracy and clarity, reflecting DevX’s style and supporting entrepreneurs in the tech sphere.

See our full editorial policy.

About Our Journalist