Search code examples
c#xunitspecflowgherkin

Is it possible to add custom logs to the Gherkin test narrative?


We have some tests that follow this pattern:

Scenario: Example Test
    Given a condition
    And another condition
    Then an action occurs
    And a condition is tested

When we run the test the Then an action occurs phrase starts a process and receives an id, which is also used by later test steps to access the same process. Is it possible to write the id (or any custom message) to the test output? Anexample of what I'm looking for is below, on the line starting with =>.

Given a condition
-> done: TestStepDefinition.GivenACondition() (2,0s)
And another condition
-> done: TestStepDefinition.GivenAnotherCondition() (0,0s)
Then an action occurs
=> Custom log message with a process id
-> done: TestStepDefinition.ThenAnActionOccurs() (6,1s)
And a condition is tested
-> done: TestStepDefinition.ThenAConditionIsTested() (1,3s)

Solution

  • The SpecFlow Output API provides a means to write additional messages to the standard output. The ISpecFlowOutputHelper interface is an object automatically registered in the SpecFlow DI container, which gives you methods for writing to the standard output while a test is running. This should be available via context injection to hooks and step definitions.

    Logging in Step Definitions

    Simply declare an ISpecFlowOutputHelper parameter in the constructor for your step definition file:

    [Binding]
    public class YourSteps
    {
        private readonly ISpecFlowOutputHelper logger;
    
        public YourSteps(ISpecFlowOutputHelper logger)
        {
            this.logger = logger;
        }
    
        [Given("X")]
        public void GivenX()
        {
            logger.WriteLine("...");
        }
    
        [When("Y")]
        public void WhenY()
        {
            logger.WriteLine("...");
        }
    
        [Then("Y")]
        public void ThenY()
        {
            logger.WriteLine("...");
        }
    }
    

    Logging in Hooks

    I haven't tried every single hook, but you can use constructor injection or method injection. Constructor injection in a hooks file works the same as in a step definition file. Plus you can use method injection, and mix-and-match in the same file:

    [Binding]
    public class Hooks
    {
        private readonly ISpecFlowOutputHelper logger;
    
        public Hooks(ISpecFlowOutputHelper logger)
        {
            this.logger = logger;
        }
    
        [BeforeScenario]
        public void BeforeScenario()
        {
            // Passed in via constructor injection
            logger.WriteLine("...");
        }
    
        [AfterScenario]
        public void AfterScenario(ISpecFlowOutputHelper output)
        { //                      ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
            // Passed in via method injection (parameter name can be anything you like)
            output.WriteLine("...");
        }// ^^^^^^
    }
    

    I haven't verified this bit, however you should be able to use method injection for hooks that must be executed from a static context. This includes hooks like [BeforeFeature]:

    [Binding]
    public class Hooks
    {
        [BeforeFeature]
        public static void BeforeFeature(ISpecFlowOutputHelper logger)
        {
            logger.WriteLine("...");
        }
    }
    

    Note that you can mix-and-match constructor injection and method injection in a single class, plus you can mix-and-match hooks executed from an instance or static context.

    There are other methods available on the ISpecFlowOutputHelper interface. If you want some standard formatting for your output messages, consider placing them in extension methods:

    namespace TechTalk.SpecFlow.Infrastructure
    {
        public static class SpecFlowOutputHelperExtensions
        {
            public static void Info(this ISpecFlowOutputHelper logger, string message)
            {
                logger.WriteLine($"[{DateTime.Now}, Info]: {message}");
            }
        }
    }
    

    In the example above, you have a new method available to simplify logging:

    [AfterScenario]
    public void AfterScenario(ISpecFlowOutputHelper logger)
    {
        logger.Info("Your info message here");