Specflow Async in constructor and Before/BeforeScenario method causes deadlock

Viewed 86

Background

In the Specflow project, this given combination causes the problem:

  1. In StepDefinition's constructor: Initialization of object whose constructor contains a call to a method which has an 'async' modifier.
  2. A method in StepDefinition is associated/declared with the [BeforeScenario] and/or [Before] attribute and it also has an 'async' modifier.

When this deadly combination is applied to the Specflow project, every time upon the test execution a deadlock problem arises.

Question

Why is this happening? And how to overcome this issue, even by maintaining the above-mentioned combination.

Demo code

Calculator.feature
Feature: Calculator
![Calculator](https://specflow.org/wp-content/uploads/2020/09/calculator.png)
Simple calculator for adding **two** numbers

Link to a feature: [Calculator]($projectname$/Features/Calculator.feature)
***Further read***: **[Learn more about how to generate Living Documentation](https://docs.specflow.org/projects/specflow-livingdoc/en/latest/LivingDocGenerator/Generating-Documentation.html)**

@mytag
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
Calculator.cs
namespace CalculatorSpec;

internal class Calculator
{
    internal int FirstNumber { get; set; }
    internal int SecondNumber { get; set; }
    internal int Total  { get; private set; }

    internal Calculator()
    {
        var printResult = "Calculator async started!".PrintAsync().GetAwaiter().GetResult();
        if (printResult)
            return;

        throw new Exception("Sync execution exception!");
    }

    internal void Add() => Total = FirstNumber + SecondNumber;
}
Printing.cs
namespace CalculatorSpec;

public static class Printing
{
    internal static async Task<bool> PrintAsync(this string message)
    {
        var task = new Task(() =>
        {
            Thread.Sleep(100);
            Console.WriteLine(message);
        });
        task.Start();
        await task;
        return true;
    }
}
CalculatorStepDefinitions.cs
using NUnit.Framework;
using TechTalk.SpecFlow.Infrastructure;

namespace CalculatorSpec;

[Binding]
public sealed class CalculatorStepDefinitions
{
    private readonly ISpecFlowOutputHelper outputHelper;
    private readonly Calculator calculator;

    public CalculatorStepDefinitions(ISpecFlowOutputHelper specFlowOutputHelperHelper)
    {
        outputHelper = specFlowOutputHelperHelper;
        calculator = new Calculator(); // Deadlock occurs
    }

    [Given("the first number is (.*)")]
    public void GivenTheFirstNumberIs(int number)
    {
        calculator.FirstNumber = number;
    }

    [Given("the second number is (.*)")]
    public void GivenTheSecondNumberIs(int number)
    {
        calculator.SecondNumber = number;
    }

    [When("the two numbers are added")]
    public void WhenTheTwoNumbersAreAdded()
    {
        calculator.Add();
    }

    [Then("the result should be (.*)")]
    public void ThenTheResultShouldBe(int result)
    {
        Assert.That(calculator.Total, Is.EqualTo(result));
    }

    [BeforeScenario]
    public async Task BeforeScenario(ScenarioContext scenarioContext)
    {
        await "Execute print async".PrintAsync();
        outputHelper.WriteLine("Started Scenario: " + scenarioContext.ScenarioInfo.Title);
    }

    [AfterScenario]
    public void AfterScenario(ScenarioContext scenarioContext)
    {
        outputHelper.WriteLine("Completed Scenario: " + scenarioContext.ScenarioInfo.Title);
    }
}
Tip

If I remove the async modifier from the method which is declared with [BeforeScenario] attribute, and also comment out the code block that awaits due to await keyword, then the problem is fixed. See below:

    [BeforeScenario]
    public /* async Task */ void BeforeScenario(ScenarioContext scenarioContext)
    {
        // await "Execute print async".PrintAsync();
        outputHelper.WriteLine("Started Scenario: " + scenarioContext.ScenarioInfo.Title);
    }

Environment

  • .NET: 6.0.300
  • SpecFlow: 3.9.22
  • NUnit: 3.13.2
1 Answers

The solution, just as you discovered, is to not call async methods in a constructor. There is an interesting discussion about async constructors that is worth reading. The consensus is:

  • Constructors should return immediately.
  • Constructors should not do real work, and anything requiring a async method call is doing "real work".
  • Async method calls become problematic when initializing readonly fields, because the field will not be initialized after the constructor is done executing (presumably some amount of time after the constructor finishes is when the field will be initialized).

SpecFlow might also not support async hooks.

The general solution is to defer calling async methods until you are executing a step definition. SpecFlow step definitions fully support async calls. If the Calculator class needs to call an async method, move that code into a property or method on the Calculator class. It doesn't belong in the constructor.

Related