Gherkin testing .NET

Specflow session - ilionx DevDays

About Me

Avatar - Max Hamulyák

Glossary

  • Behavior Driven Development (BBD)

  • Domain Specific Language (DSL)

Gherkin

  • Unambiguous executable specification

  • Automated testing using Cucumber

  • Document how the system actually behaves

Cucumber / Gherkin Logo
Feature: Guess the word

  # The first example has two steps
  Scenario: Maker starts a game
    When the Maker starts a game
    Then the Maker waits for a Breaker to join

  # The second example has three steps
  Scenario: Breaker joins a game
    Given the Maker has started a game with the word "silky"
    When the Breaker joins the Maker's game
    Then the Breaker must guess a word with 5 characters

Keywords

  • Feature

  • Rule (as of Gherkin 6)

  • Example (or Scenario)

  • Given, When, Then, And, But for steps (or *)

  • Background

  • Scenario Outline (or Scenario Template)

  • Examples (or Scenarios)

# language: no
Funksjonalitet: Gjett et ord

  Eksempel: Ordmaker starter et spill
    Når Ordmaker starter et spill
    Så må Ordmaker vente på at Gjetter blir med

  Eksempel: Gjetter blir med
    Gitt at Ordmaker har startet et spill med ordet "bløtt"
    Når Gjetter blir med på Ordmakers spill
    Så må Gjetter gjette et ord på 5 bokstaver

Gherkin Syntax

Feature: Calculator
 
Calculator for adding two numbers
 
Scenario: Add two numbers
    Add two numbers with the calculator
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
Support for multi-line text
Given a blog post named "Random" with Markdown body
  """
  Some Title, Eh?
  ===============
  Here is the first paragraph of my blog post. Lorem ipsum dolor sit amet,
  consectetur adipiscing elit.
  """
Support for complex structures
Given the following users exist:
  | name   | email              | twitter         |
  | Aslak  | aslak@cucumber.io  | @aslak_hellesoy |
  | Julien | julien@cucumber.io | @jbpros         |
  | Matt   | matt@cucumber.io   | @mattwynne      |
Do not do this
Scenario: eat 5 out of 12
  Given there are 12 cucumbers
  When I eat 5 cucumbers
  Then I should have 7 cucumbers

Scenario: eat 5 out of 20
  Given there are 20 cucumbers
  When I eat 5 cucumbers
  Then I should have 15 cucumbers
Prefer this instead
Scenario Outline: eating
  Given there are <start> cucumbers
  When I eat <eat> cucumbers
  Then I should have <left> cucumbers

  Examples:
    | start | eat | left |
    |    12 |   5 |    7 |
    |    20 |   5 |   15 |
Keywords are not used for uniqueness
Given there is money in my account
Then there is money in my account

Given my account has a balance of £430
Then my account should have a balance of £430

Specflow

  • BDD for .NET

    • Official version of Cucumber for .NET

    • Open source since 2009 (Gáspár Nagy, TechTalk)

  • Execute scenarios using automation code

Specflow Logo
Specflow Layers

Bindings

The glue between Gherkin feature and test code is done via [Binding]

[Binding]
public class StepDefinitions
{
	...
}
[When(@"^I perform a simple search on '(.*)'$")]
public void WhenIPerformASimpleSearchOn(string searchTerm)
{
    var controller = new CatalogController();
    actionResult = controller.Search(searchTerm);
}
[When("I perform a simple search on {string}")]
[When("I search for {string}")]
public void WhenIPerformASimpleSearchOn(string searchTerm)
{
  ...
}
Specflow Binding

Use the [Scope] attribute to define the scope:

[Scope(
    Tag = "mytag", 
    Feature = "feature title",
    Scenario = "scenario title"
)]
[Binding]
public class StepDefinitions
{
	...
}
[When(@"I perform a simple search on '(.*)'",
    Scope(Tag = "controller"))]
public void WhenIPerformASimpleSearchOn(string searchTerm)
{
    var controller = new CatalogController();
    actionResult = controller.Search(searchTerm);
}

[When(@"I perform a simple search on '(.*)'"),
    Scope(Tag = "web")]
public void PerformSimpleSearch(string title)
{
    selenium.GoToThePage("Home");
    selenium.Type("searchTerm", title);
    selenium.Click("searchButton");
}

Hooks

  • [BeforeFeature]/[AfterFeature]

  • [BeforeScenario]/[AfterScenario]

  • [BeforeScenarioBlock]/[AfterScenarioBlock]

  • [BeforeStep]/[AfterStep]

Sharing Data between Bindings

Different [Binding] need to share data

Feature: ContextSharing
	Simple calculator for adding two numbers

Scenario: Add two numbers
	Given the first number is 50
	And the second number is 70
	When the two numbers are added
	Then the result should be 120
public class CalculatorContext
{
    public int? FirstNumber { get; set; }
    public int? SecondNumber { get; set; }
    public int? Result { get; set; }
}
[Binding]
public sealed class ContextSharing
{
    private readonly CalculatorContext _calculatorContext;

    public ContextSharing(CalculatorContext calculatorContext)
    {
        _calculatorContext = calculatorContext;
    }

    [Given("the first number is (.*)")]
    public void GivenTheFirstNumberIs(int number)
    {
        _calculatorContext.FirstNumber = number;
    }
}
[Binding]
public class WebDriverSupport
{
  private readonly IObjectContainer objectContainer;

  public WebDriverSupport(IObjectContainer objectContainer)
  {
    this.objectContainer = objectContainer;
  }

  [BeforeScenario]
  public void InitializeWebDriver()
  {
    var webDriver = new FirefoxDriver();
    objectContainer.RegisterInstanceAs<IWebDriver>(webDriver);
  }
}
[Binding]
public class Binding
{
    ScenarioContext _scenarioContext;

    public Binding(ScenarioContext scenarioContext)
    {
        _scenarioContext = scenarioContext;
    }

    [BeforeScenario("web")]
    public static void BeforeWebScenario()
    {
        if(_scenarioContext.ScenarioInfo.Tags.Contains("automated"))
            StartSelenium();
    }
}

Step Argument Conversions

Specflow param conversion 1
More natural language
    Given we have '0' items in stock:
    Given we have 'no' items in stock:
Specflow param conversion 2
public class Conference
{
    public string? Name { get; set; }
    public string? Location { get; set; }
    public DateTimeOffset? From { get; set; }
    public DateTimeOffset? To { get; set; }
}

Automation

    [Given]
    public void GivenTheFollowingConferences1(Table table)
    {
    }

Horizontal Tables

        Given the following conference1
          | Name    | Location | From       | To         |
          | DevDays | Arnhem   | 2023-05-12 | 2023-05-13 |

Vertical Tables

        Given the following conference1
          | Key      | Value   |
          | Name     | DevDays |
          | Location | Arnhem  |
Specflow table conversion 1
    [Given]
    public void GivenTheFollowingConferences1(Table table)
    {
        foreach (var tableRow in table.Rows)
        {
            string name = tableRow["Name"];
            string location = tableRow["Location"];
            string from = tableRow["From"];
            string to = tableRow["To"];
            
            // ...
        }
    }
    [Given]
    public void GivenTheFollowingConferences2(Table table)
    {
        foreach (var tableRow in table.Rows)
        {
            string name = tableRow.GetString("Name");
            string location = tableRow.GetString("Location");
            DateTimeOffset from = tableRow.GetDateTime("From");
            DateTimeOffset to = tableRow.GetDateTime("To");
            
            // ...
        }
    }
    [Given]
    public void GivenTheFollowingConferences3(Table table)
    {
        var conferences = table.CreateSet<Conference>();
    }
    [Given]
    public void GivenTheFollowingConference1(Table table)
    {
        var conference = table.CreateInstance<Conference>();
        
    }
    [Given]
    public void GivenTheFollowingConferences(List<Conference> conferences)
Specflow table conversion 2
What will be the value of location?
        Given the following conference1
          | Name    | Location | From       | To         |
          | DevDays |          | 2023-05-12 | 2023-05-13 |
        Given the following conference1
          | Name    | From       | To         |
          | DevDays | 2023-05-12 | 2023-05-13 |
        Given the following conference1
          | Name    | Location | From       | To         |
          | DevDays | <null>   | 2023-05-12 | 2023-05-13 |
[Binding]
public class GlobalHooks
{
    [BeforeTestRun]
    public static void BeforeTestRun()
    {
        Service
            .Instance
            .ValueRetrievers
            .Register(new NullValueRetriever(Constants.NullIndicator));
    }
}
Scenario Outline: Technical Id
    Given we have the following technical id '<Id>':
    
    Scenarios: 
        | Id                                   |
        | 7e53b915-a454-4337-afae-0cf0e69814a2 |
    [Given(@"we have the following technical id '(.*)':")]
    public void GivenTheFollowingTechnicalId(Guid id)
        _specFlowOutputHelper.WriteLine($"id: '{id}'");
Scenario Outline: Technical Id
    Given we have the following technical id '<Id>':
    
    Scenarios: 
        | Id                                   |
        | 7e53b915-a454-4337-afae-0cf0e69814a2 |
        | 7e53b915-a454-4337-afae              |
        | 7e53b915-a454-4337                   |
        | 7e53b915-a454                        |
        | 7e53b915                             |
        | 7                                    |

Living Documentation

living doc 1

living doc 2

living doc 3

living doc 4

Tips & Tricks

Glossary

  • Business Entities

  • Tags

Brief Principles

  • Business language

  • Real data

  • Intention revealing

  • Essential

  • Focused

Write a good when

Imagine it’s 1922

More Readable Scenarios

More Readable Scenarios

Given an open account with 100 USD balance
When the account receives a 20 USD payment
Then the account balance will be 120 USD

More Readable Scenarios

Given a <status> account with <old balance> balance
When the account receives an <amount> payment
Then the account balance will be <new balance>

Examples:

| status | old balance | amount | new balance |
| open   | 100 USD     | 20 USD |     120 USD | 

More Readable Scenarios

Given an "open" account with "100 USD" balance
When the account receives a payment of "20 USD"
Then the account balance will be "120 USD"

Business Language

Consider target audience

Questions?

Source Code QR
Slide QR