Easy Approach to Requirements Syntax and the segue to Behavior Driven Development
I was attending a conference six months ago and listened to a talk about quality. During the talk, I was introduced to EARS — Easy Approach to Requirements Syntax. This way of writing requirements struck a chord with me, given my prior experience reading and writing requirement specifications.
When doing agile development, we write User Stories and define Acceptance Criteria.
As a
<role>
, I want<goal/desire>
so that<benefit>
When doing BDD, we follow this format:
In order to
<receive benefit>
, as a<role>
, I want<goal/desire>
It’s not always easy to go from user stories and acceptance criteria to start writing tests.
I think that with Easy Approach to Requirements Syntax in place, it will be easier to do Behavior Driven Development.
When doing Hypothesis Driven Development, we follow this format:
We believe
<this capability>
Will result in
<this outcome>
We will know we have succeeded when
<we see a measurable signal>
So my hypothesis is:
I believe using Easy Approach to Requirements Syntax
Will result in easier implementation of Behavior Driven Development
I will know I have succeeded when business people can actually write the (SpecFlow) feature files themselves ☺
Easy Approach to Requirements Syntax
EARS was created during a case study at Rolls-Royce on requirements for aircraft engine control systems.
They identified eight major problems with writing requirements in an unstructured natural language:
- Ambiguity (a word or phrase has two or more different meanings).
- Vagueness (lack of precision, structure and/or detail).
- Complexity (compound requirements containing complex sub-clauses and/or several interrelated statements).
- Omission (missing requirements, particularly requirements to handle unwanted behavior).
- Duplication (repetition of requirements that are defining the same need).
- Wordiness (use of an unnecessary number of words).
- Inappropriate implementation (statements of how the system should be built, rather than what it should do).
- Untestability (requirements that cannot be proven true or false when the system is implemented).
To overcome or reduce the effects of these problems they came up with a rule set with five simple templates.
Requirements are divided into five types:
- Ubiquitous
- Event-driven
- State-driven
- Unwanted behaviors
- Optional features
Ubiquitous
The
<system name>
shall<system response>
Event-driven
When
<optional preconditions>
<trigger>
, the<system name>
shall<system response>
State-driven
While
<in a specific state>
, the<system name>
shall<system response>
Unwanted behaviors
If
<optional preconditions>
<trigger>
, then the<system name>
shall<system response>
Optional features
Where
<feature is included>
, the<system name>
shall<system response>
The Stack Class
Let’s put this to the test with the Stack<T>
Class as the example.
This is some of the documentation from MSDN:
Represents a variable size last-in-first-out (LIFO) collection of instances of the same specified type.
The capacity of the
Stack<T>
is the number of elements that theStack<T>
can store.Count
is the number of elements that are actually in theStack<T>
.
Three main operations can be performed on a
Stack<T>
and its elements:
Push
inserts an element at the top of theStack<T>
.Pop
removes an element from the top of theStack<T>
.Peek
returns an element that is at the top of theStack<T>
but does not remove it from theStack<T>
.
If we were to write a User Story in BDD format:
In order to store instances of the same specified type in last-in-first-out (LIFO) sequence
As a developer
I want to use a
Stack<T>
If we were to write requirements with EARS templates:
Ubiquitous
The
Stack<T>
shall store instances of the same specified type in last-in-first-out (LIFO) order.
The
Stack<T>
shall return the number of elements contained when the propertyCount
is invoked.
Event-driven
When the method
Push
is invoked, theStack<T>
shall insert the element at the top.
When the method
Pop
is invoked, theStack<T>
shall remove and return the element at the top.
When the method
Peek
is invoked, theStack<T>
shall return the element at the top without removing it.
State-driven
While an element is present, the
Stack<T>
shall returntrue
when the methodContains
is invoked.
While an element is not present, the
Stack<T>
shall returnfalse
when the methodContains
is invoked.
Unwanted behaviors
If empty and the method
Pop
is invoked, then theStack<T>
shall throwInvalidOperationException
.
If empty and the method
Peek
is invoked, then theStack<T>
shall throwInvalidOperationException
.
Optional features
Where instantiated with a specified collection, the
Stack<T>
shall be prepopulated with the elements of the collection.
Behavior Driven Development
Let’s take this to the next level with BDD and SpecFlow.
Feature: Stack<T> | |
In order to store instances of the same specified type in last-in-first-out (LIFO) sequence | |
As a developer | |
I want to use a Stack<T> | |
@Ubiquitous | |
Scenario: The Stack<T> shall store instances of the same specified type in last-in-first-out (LIFO) order | |
Given an empty stack | |
When the pushing the elements: | |
| Element | | |
| One | | |
| Two | | |
| Three | | |
Then the elements should be popped in the order: | |
| Element | | |
| Three | | |
| Two | | |
| One | | |
@Ubiquitous | |
Scenario: The Stack<T> shall return the number of elements contained when the property Count is invoked | |
Given a stack with the elements: | |
| Element | | |
| One | | |
| Two | | |
| Three | | |
When counting the elements | |
Then the result should be 3 | |
@EventDriven | |
Scenario: When the method Push is invoked, the Stack<T> shall inserts the element at the top | |
Given a stack with the elements: | |
| Element | | |
| One | | |
| Two | | |
| Three | | |
When pushing the element "Four" | |
Then the element "Four" should be on top | |
@EventDriven | |
Scenario: When the method Pop is invoked, the Stack<T> shall remove and return the element at the top | |
Given a stack with the elements: | |
| Element | | |
| One | | |
| Two | | |
| Three | | |
When popping the element on top | |
Then the result should be "Three" | |
And the element "Two" should be on top | |
@EventDriven | |
Scenario: When the method Peek is invoked, the Stack<T> shall return the element at the top without removing it | |
Given a stack with the elements: | |
| Element | | |
| One | | |
| Two | | |
| Three | | |
When peeking at the element on top | |
Then the result should be "Three" | |
And the element "Three" should be on top | |
@StateDriven | |
Scenario: While an element is present, the Stack<T> shall return true when the method Contains is invoked | |
Given a stack with the elements: | |
| Element | | |
| One | | |
| Two | | |
| Three | | |
When determining if the stack contains the element "Three" | |
Then the result should be true | |
@StateDriven | |
Scenario: While an element is not present, the Stack<T> shall return false when the method Contains is invoked | |
Given a stack with the elements: | |
| Element | | |
| One | | |
| Two | | |
| Three | | |
When determining if the stack contains the element "Four" | |
Then the result should be false | |
@UnwantedBehavior | |
Scenario: If empty and the method Pop is invoked, then the Stack<T> shall throw InvalidOperationException | |
Given an empty stack | |
When popping the element on top | |
Then InvalidOperationException should be thrown | |
@UnwantedBehavior | |
Scenario: If empty and the method Peek is invoked, then the Stack<T> shall throw InvalidOperationException | |
Given an empty stack | |
When peeking at the element on top | |
Then InvalidOperationException should be thrown | |
@Optional | |
Scenario: Where instantiated with a specified collection, the Stack<T> shall be pre-populated with the elements of the collection | |
Given a stack with the elements: | |
| Element | | |
| One | | |
| Two | | |
| Three | | |
Then the stack should contain the elements: | |
| Element | | |
| Three | | |
| Two | | |
| One | |
- Each requirement has its own scenario
- I’ve tagged the scenarios with the type of requirement for clarity
using Should; | |
using System; | |
using System.Collections.Generic; | |
using System.Linq; | |
using TechTalk.SpecFlow; | |
namespace ConductOfCode.Steps | |
{ | |
[Binding] | |
public class StackSteps | |
{ | |
private Stack<string> stack; | |
private string result; | |
private int count; | |
private bool contains; | |
private Exception exception; | |
[Given(@"an empty stack")] | |
public void GivenAnEmptyStack() | |
{ | |
stack = new Stack<string>(); | |
} | |
[Given(@"a stack with the elements:")] | |
public void GivenAStackWithTheElements(Table table) | |
{ | |
var elements = table.Rows.Select(x => x.Values.First()).ToList(); | |
stack = new Stack<string>(elements); | |
} | |
[When(@"the pushing the elements:")] | |
public void WhenThePushingTheElements(Table table) | |
{ | |
var elements = table.Rows.Select(x => x.Values.First()).ToList(); | |
foreach (var element in elements) | |
{ | |
stack.Push(element); | |
} | |
} | |
[When(@"counting the elements")] | |
public void WhenCountingTheElements() | |
{ | |
count = stack.Count; | |
} | |
[When(@"pushing the element ""(.*)""")] | |
public void WhenPushingTheElement(string element) | |
{ | |
stack.Push(element); | |
} | |
[When(@"popping the element on top")] | |
public void WhenPoppingTheElementOnTop() | |
{ | |
try | |
{ | |
result = stack.Pop(); | |
} | |
catch (Exception ex) | |
{ | |
exception = ex; | |
} | |
} | |
[When(@"peeking at the element on top")] | |
public void WhenPeekingAtTheElementOnTop() | |
{ | |
try | |
{ | |
result = stack.Peek(); | |
} | |
catch (Exception ex) | |
{ | |
exception = ex; | |
} | |
} | |
[When(@"determining if the stack contains the element ""(.*)""")] | |
public void WhenDeterminingIfTheStackContainsTheElement(string element) | |
{ | |
contains = stack.Contains(element); | |
} | |
[Then(@"the elements should be popped in the order:")] | |
public void ThenTheElementsShouldBePoppedInTheOrder(Table table) | |
{ | |
var elements = table.Rows.Select(x => x.Values.First()).ToList(); | |
foreach (var element in elements) | |
{ | |
element.ShouldEqual(stack.Pop()); | |
} | |
} | |
[Then(@"the result should be (.*)")] | |
public void ThenTheResultShouldBe(int expected) | |
{ | |
count.ShouldEqual(expected); | |
} | |
[Then(@"the element ""(.*)"" should be on top")] | |
public void ThenTheElementShouldBeOnTop(string element) | |
{ | |
element.ShouldEqual(stack.Peek()); | |
} | |
[Then(@"the result should be ""(.*)""")] | |
public void ThenTheResultShouldBe(string expected) | |
{ | |
result.ShouldEqual(expected); | |
} | |
[Then(@"the result should be true")] | |
public void ThenTheResultShouldBeTrue() | |
{ | |
contains.ShouldBeTrue(); | |
} | |
[Then(@"the result should be false")] | |
public void ThenTheResultShouldBeFalse() | |
{ | |
contains.ShouldBeFalse(); | |
} | |
[Then(@"InvalidOperationException should be thrown")] | |
public void ThenInvalidOperationExceptionShouldBeThrown() | |
{ | |
exception.ShouldBeType<InvalidOperationException>(); | |
} | |
[Then(@"the stack should contain the elements:")] | |
public void ThenTheStackShouldContainTheElements(Table table) | |
{ | |
var elements = table.Rows.Select(x => x.Values.First()).ToList(); | |
foreach (var element in elements) | |
{ | |
stack.Contains(element).ShouldBeTrue(); | |
} | |
} | |
} | |
} |
In my opinion, it was easy to write the tests. I copy-and-pasted the requirement to the SpecFlow feature file and then I knew exactly how many scenarios I needed to implement. I think the examples in the scenarios makes the requirements easier to understand and reason about. Maybe this should be called Requirements by Example?
The BDD Cycle
When implementing the production code, we can use The BDD Cycle described in The RSpec Book.
- A photo of page 10 from my copy of The RSpec Book
- As a .NET developer, you can replace Cucumber with SpecFlow and RSpec with Machine.Specifications
The BDD Cycle introduces two levels of testing. We can use SpecFlow to focus on the high-level behavior, the requirements. And use Machine.Specifications to focus on more granular behavior, unit testing code in isolation.
Resources
-
Easy approach to requirements syntax (EARS) by Alistair Mavin et al. The six-page research paper.
-
EARS quick reference sheet [PDF] from Aalto University. A two-page summary.
-
EARS: The Easy Approach to Requirements Syntax [PDF] by John Terzakis. A 66-page presentation on EARS and how it is used at Intel.
-
The source code for the example in this blog post: GitHub