SpecFlow, a.k.a. “Cucumber for .NET,” is a leading BDD test automation framework for .NET. Created by Gáspár Nagy and maintained as a free, open source project on GitHub by TechTalk, SpecFlow presently has almost 3 million total NuGet downloads. I’ve used it myself at a few companies, and, I must say as an automationeer, it’s awesome! SpecFlow shares a lot in common with other Cucumber frameworks like Cucumber-JVM, but it is not a knockoff – it excels in many ways. Below are five features I love about SpecFlow.
#1: Declarative Specification by Example
SpecFlow is a behavior-driven test framework. Test cases are written as Given-When-Then scenarios in Gherkin “.feature” files. For example, imagine testing a cucumber basket:
Feature: Cucumber Basket As a gardener, I want to carry many cucumbers in a basket, So that I don’t drop them all. @cucumber-basket Scenario: Fill an empty basket with cucumbers Given the basket is empty When "10" cucumbers are added to the basket Then the basket is full
Notice a few things:
- It is declarative in that steps indicate what should be done at a high level.
- It is concise in that a full test case is only a few lines long.
- It is meaningful in that the coverage and purpose of the test are intuitively obvious.
- It is focused in that the scenario covers only one main behavior.
Gherkin makes it easy to specify behaviors by example. That way, everybody can understand what is happening. C# code will implement each step in lower layers. Even if your team doesn’t do the full-blown BDD process, using a BDD framework like SpecFlow is still great for test automation. Test code naturally abstracts into separate layers, and steps are reusable, too!
#2: Context is King
Safely sharing data (e.g., “context”) between steps is a big challenge in BDD test frameworks. Using static variables is a simple yet terrible solution – any class can access them, but they create collisions for parallel test runs. SpecFlow provides much better patterns for sharing context.
Context injection is SpecFlow’s simple yet powerful mechanism for inversion of control (using BoDi). Any POCOs can be injected into any step definition class, either using default values or using a specific initialization, by declaring the POCO as a step def constructor argument. Those instances will also be shared instances, meaning steps across different classes can share the same objects! For example, steps for Web tests will all need a reference to the scenario’s one WebDriver instance. The context-injected objects are also created fresh for each scenario to protect test case independence.
Another powerful context mechanism is ScenarioContext. Every scenario has a unique context: title, tags, feature, and errors. Arbitrary objects can also be stored in the context object like a Dictionary, which is a simple way to pass data between steps without constructor-level context injection. Step definition classes can access the current scenario context using the static ScenarioContext.Current variable, but a better, thread-safe pattern is to make all step def classes extend the Steps class and simply reference the ScenarioContext instance variable.
#3: Hooks for Any Occasion
Hooks are special methods that insert extra logic at critical points of execution. For example, WebDriver cleanup should happen after a Web test scenario completes, no matter the result. If the cleanup routine were put into a Then step, then it would not be executed if the scenario had a failure in a When step. Hooks are reminiscent of Aspect-Oriented Programming.
Most BDD frameworks have some sort of hooks, but SpecFlow stands out for its hook richness. Hooks can be applied before and after steps, scenario blocks, scenarios, features, and even around the whole test run. (Cucumber-JVM, by contrast, does not support global hooks.) Hooks can be selectively applied using tags, and they can be assigned an order if a project has multiple hooks of the same type. Hook methods will also be picked up from any step definition class. SpecFlow hooks are just awesome!
#4: Thorough Outline Templating
Scenario Outlines are a standard part of Gherkin syntax. They’re very useful for templating scenarios with multiple input combinations. Consider the cucumber basket again:
Feature: Cucumber Basket Scenario Outline: Add cucumbers to the basket Given the basket has "<initial>" cucumbers When "<some>" cucumbers are added to the basket Then the basket has "<total>" cucumbers Examples: Counts | initial | some | total | | 1 | 2 | 3 | | 5 | 3 | 8 |
All BDD frameworks can parametrize step inputs (shown in double quotes). However, SpecFlow can also parametrize the non-input parts of a step!
Feature: Cucumber Basket Scenario Outline: Use the cucumber basket Given the basket has "<initial>" cucumbers When "<some>" cucumbers are <handled-with> the basket Then the basket has "<total>" cucumbers Examples: Counts | initial | some | handled-with | total | | 1 | 2 | added to | 3 | | 5 | 3 | removed from | 2 |
The step definitions for the add and remove steps are separate. The step text for the action is parametrized, even though it is not a step input:
[When(@"""(\d+)"" cucumbers are added to the basket")] public void WhenCucumbersAreAddedToTheBasket(int count) { /* */ } [When(@"""(\d+)"" cucumbers are removed from the basket")] public void WhenCucumbersAreRemovedFromTheBasket(int count) { /* */ }
That’s cool!
#5: Test Thread Affinity
SpecFlow can use any unit test runner (like MsTest, NUnit, and xUnit.net), but TechTalk provides the official SpecFlow+ Runner for a licensed fee. I’m not associated with TechTalk in any way, but the SpecFlow+ Runner is worth the cost for enterprise-level projects. It has a friendly command line, a profile file to customize execution, parallel execution, and nice integrations.
The major differentiator, in my opinion, is its test thread affinity feature. When running tests in parallel, the major challenge is avoiding collisions. Test thread affinity is a simple yet powerful way to control which tests run on which threads. For example, consider testing a website with user accounts. No two tests should use the same user at the same time, for fear of collision. Scenarios can be tagged for different users, and each thread can have the affinity to run scenarios for a unique user. Some sort of parallel isolation management like test thread affinity is absolutely necessary for test automation at scale. Given that the SpecFlow+ Runner can handle up to 64 threads (according to TechTalk), massive scale-up is possible.
But Wait, There’s More!
SpecFlow is an all-around great test automation framework, whether or not your team is doing full BDD. Feel free to add comments below about other features you love (or *gasp* hate) about SpecFlow!
Thanks for another great blog post and really enjoyable read. SpecFlow is a great tool, for sure. I do believe that it does have a cost of ownership that can be non-trivial for larger test code bases.
In particular, the way it joins the code behind the steps together can make it hard to navigate and comprehend the test code base. Some tools are more developer friendly and can reduce this pain whilst still allowing a specification driven approach to the testing. For example, in the .NET space, XBehave is a good example and doesn’t change the way a developer would arrange their code too much.
That said, I believe that the additional investment there can be paid back many times over if the specifications are widely owned by other disciplines and through business teams. This, of course, is exactly what should be happening but I’ve seen plenty of times where specifications are sadly seen as just testing artefacts and are left to those writing the systems.
I’ve taken pain from trying to use BoDi as a general DI tool also. It believe it’s designed to be used just within the text context. Perhaps that’s changed (or perhaps I was just using in incorrectly, lol!)?
LikeLiked by 1 person
Good insights. Thanks for sharing!
LikeLike