Table of Contents

Advanced Concepts in Reqnroll with Examples

For most of the scenarios, the basic concepts of Reqnroll are sufficient. However, there are some advanced concepts that can be used to write more complex scenarios. In this article, we will discuss some of these advanced concepts with examples.

Scenario Outline

The Scenario Outline keyword can be used to run the same scenario multiple times with different test data. The test data is provided in the Examples section of the scenario.

It's the equivalent of the theory attribute in xUnit.

Feature: Login Functionality

Scenario Outline: User Login with Different Credentials
  Given the user is on the login page
  When the user enters <username> and <password>
  Then the login <status> should be displayed

Examples:
  | username | password | status   |
  | user1    | pass123  | success  |
  | user2    | wrongpass | failure  |

In the above example, the Scenario Outline is used to test the login functionality with different credentials. The Examples section provides the test data for the scenario.

public class LoginSteps
{
    [Given(@"the user is on the login page")]
    public void GivenTheUserIsOnTheLoginPage()
    {
        // Code to navigate to the login page
    }

    [When(@"the user enters (.*) and (.*)")]
    public void WhenTheUserEntersUsernameAndPassword(string username, string password)
    {
        // Code to enter the username and password
    }

    [Then(@"the login (.*) should be displayed")]
    public void ThenTheLoginStatusShouldBeDisplayed(string status)
    {
        // Code to verify the login status
    }
}

Background

The Background keyword can be used to define a set of steps that are common to all scenarios in a feature. The steps defined in the Background section are executed before each scenario.

Feature: Product Inventory

Background:
  Given there are 10 units of "Product A" in stock

Scenario: Selling a Product
  When a customer purchases 2 units of "Product A"
  Then the stock of "Product A" should be 8

Scenario: Restocking a Product
  When the store manager adds 5 units of "Product A" to the inventory
  Then the stock of "Product A" should be 15

Best Practices

Keep it Concise: The Background section should contain only essential setup steps to avoid excessive complexity.

Avoid Business Logic: The Background section is primarily for setup steps. Avoid including business logic or verifications; those should be placed in the individual scenarios.

Data tables


Feature: Shopping Cart

Scenario: Calculating Total Price with Discounts
    Given the user has the following items in their shopping cart:
        | Name        | Quantity | Price  |
        | Apples      | 5        | 2.00  |
        | Bananas     | 3        | 1.50  |
        | Orange Juice| 2        | 3.50  |
    When the user proceeds to checkout
    Then the total price should be calculated as 15.50
    But a discount of 10% should be applied
    And the final payable amount should be 13.95

In the above example, the data table is used to provide the test data for the scenario.

public class ShoppingCartSteps
{
    [Given(@"the user has the following items in their shopping cart:")]
    public void GivenTheUserHasTheFollowingItemsInTheirShoppingCart(Table table)
    {
        // Code to add items to the shopping cart
    }

    [When(@"the user proceeds to checkout")]
    public void WhenTheUserProceedsToCheckout()
    {
        // Code to proceed to checkout
    }

    [Then(@"the total price should be calculated as (.*)")]
    public void ThenTheTotalPriceShouldBeCalculatedAs(decimal totalPrice)
    {
        // Code to verify the total price
    }

    [Then(@"a discount of (.*) should be applied")]
    public void ThenADiscountOfShouldBeApplied(decimal discount)
    {
        // Code to verify the discount
    }

    [Then(@"the final payable amount should be (.*)")]
    public void ThenTheFinalPayableAmountShouldBe(decimal payableAmount)
    {
        // Code to verify the payable amount
    }
}

Example 4: Using Tags

Overview Tags are labels or markers that can be assigned to features or scenarios to categorize and filter them. Tags are prefixed with the @ symbol and can be used for various purposes, such as test categorization, conditional execution, or documentation.

Syntax Tags can be added to the Feature, Scenario, or Scenario Outline sections by prefixing them with the @ symbol. Multiple tags can be applied to a single feature or scenario.

@Permissions
Feature: Access to the command's details

@LoggedIn
  Scenario: User can access the command's details
    Given the user is logged in
    When the user clicks on the View Details button
    Then the command details should be displayed

@LoggedOut
  Scenario: User cannot access the command's details
    Given the user is logged out
    When the user clicks on the View Details button
    Then the user should be redirected to the login page

Usage in Test Execution During test execution, tags can be used to include or exclude specific scenarios. For example, using Reqnroll's test runner commands:

To run scenarios with a specific tag:

dotnet test --filter "Tags=Permissions"

To exclude scenarios with a specific tag:

dotnet test --filter "Tags!=Permissions"

Example 5: Using Hooks

hooks provide a way to execute code before and after various events in the test execution lifecycle. This allows you to set up or clean up datas, perform additional logging, or modify behavior around scenarios.

Types of Hooks

BeforeScenario: Executes before each scenario.

AfterScenario: Executes after each scenario.

BeforeFeature: Executes before each feature.

AfterFeature: Executes after each feature.

BeforeTestRun: Executes once before the entire test run.

AfterTestRun: Executes once after the entire test run

Parameterized Hooks

Hooks can take parameters to access information about the scenario or feature being executed.

[BeforeScenario]
public void BeforeScenario(ScenarioContext scenarioContext)
{
    // Access scenario information
    var scenarioTitle = scenarioContext.ScenarioInfo.Title;
}

Conditional Execution

You can use conditional logic within hooks to execute specific actions based on certain conditions.

    [BeforeScenario("myTag")]
public void BeforeScenarioWithTag()
{
    // Code to be executed before scenarios with the "myTag" tag
}

Execution Order

Hooks are executed in a predefined order. For example, BeforeTestRun hooks are executed first, followed by BeforeFeature hooks, BeforeScenario hooks, scenario execution, and then the corresponding After hooks.

Concrete Example

Feature: Order Processing

@InStock
Scenario: Process Order for In-Stock Item
    Given there are 10 units of "Product A" in stock
    When the user places an order for 2 units of "Product A"
    Then the order should be processed successfully

@OutOfStock
Scenario: Process Order for Out-of-Stock Item
    Given there are 0 units of "Product B" in stock
    When the user places an order for 1 unit of "Product B"
    Then the user should be notified of the out-of-stock status

in the above example, we can use hooks to set up the initial context for the scenarios.

Hooks are implemented as methods in a separate class or within the same class as step definitions. But it is recommended to keep them in a separate class for a better organization of the code.

OrderProcessingHooks.cs

[Binding]
public class OrderProcessingHooks
{
    [BeforeScenario("InStock")]
    public void SetupInStockProduct()
    {
        // Code to set up the initial context for the scenario
        // Like creating a product in the database
    }

    [BeforeScenario("OutOfStock")]
    public void SetupOutOfStockProduct()
    {
        // Code to set up the initial context for the scenario
        // Empty the stock of the product
    }

    [AfterScenario("InStock", "OutOfStock")]
    public void AfterTest()
    {
        // Code to run after any scenario with the @InStock or @OutOfStock tag
        Console.WriteLine("Scenario execution completed.");
    }
}

OrderProcessingStepDefinition.cs


[Binding]

public class OrderProcessingSteps
{
    [Given(@"there are (.*) units of ""(.*)"" in stock")]
    public void GivenThereAreUnitsOfInStock(int units, string productName)
    {
        // Code to set up the initial context for the scenario
    }

    [When(@"the user places an order for (.*) units of ""(.*)""")]
    public void WhenTheUserPlacesAnOrderForUnitsOf(int units, string productName)
    {
        // Code to place the order
    }

    [Then(@"the order should be processed successfully")]
    public void ThenTheOrderShouldBeProcessedSuccessfully()
    {
        // Code to verify the order status
    }

    [Then(@"the user should be notified of the out-of-stock status")]
    public void ThenTheUserShouldBeNotifiedOfTheOut_Of_StockStatus()
    {
        // Code to verify the out-of-stock status
    }
}

Force execution order

If you want to force the execution of an hook's method, you can use the Order property of the Binding attribute.


[Binding]

public class OrderProcessingHooks
{
    [BeforeScenario(Order = 1)]
    public void DeleteCreatedStock()
    {
        // Code to set up the initial context for the scenario
        // Empty the stock of the product
    }
}

In the case below the method DeleteCreatedStock will be executed despite the associated scenario is failed.