Using Specflow without the unit testing frameworks

Viewed 43

So for my current project I need to perform several tests on a (physical) system. Within my company we have this testing framework, which provides several features out of the box, such as writing my results to a database and such.

Now I've heard about BDD and I found that a gherkin way of writing these tests would be really nice and clean. So I started looking into Specflow, which looks really good. Only problem with Specflow is that you are required to use a Unit Testing framework. This is not at all what I want, since my own framework already provides all this stuff. Starting a test for example needs to be done by an operator from a GUI. So the unit testing framework is not needed at all.

Now my question is whether I can use Specflow simply to generate these tests (within their own test class or something) and keep using my own framework.

So as an example, what I'm looking for is something along these lines:

Given that the machine is in base state
When we open the valve
Then the valve sensor should have value higher than 450

Then have a StepDefinition file, which will contain the actual machine interface (sample code ofc)

public void MachineInBaseState()
{
    machine.GoToBaseState();
}
public void ValvePosition(bool open)
{
    machine.SetValve(open);
}
public void CheckValveSensorPosition(int valueToCheckAgainst)
{
    testResults.Add(new ValueResult(value:machine.GetSensorValue(),min:valueToCheckAgainst,max:double.NaN));
}

Then during build of the project, to have the gherkin file be generated in such a way that my own testing framework can pick up the generated testresults.

I know that this may not be something that a lot of people do, but maybe someone has seen something like it before.

Another way of course is to write a source generator, but that will take a lot more time.

2 Answers

You would need to build your own UnitTestProvider for your framework.

One for compile time (when the code behind files are generated): https://github.com/SpecFlowOSS/SpecFlow/tree/master/TechTalk.SpecFlow.Generator/UnitTestProvider

This is what the content of the feature.cs files generates.

And one for runtime: like the xUnit one https://github.com/SpecFlowOSS/SpecFlow/blob/master/Plugins/TechTalk.SpecFlow.xUnit.SpecFlowPlugin/XUnitRuntimeProvider.cs

All this can be plugged into SpecFlow via Plugins, but it is not the easiest to do.

After looking at the great advice below, some research and some trial and error, I eventually went another way.

My current solution is to use the Gherkin NuGet package to parse the .feature files, then I use the parsed files to generate my own .g.cs code, which can be easily integrated in my own framework.

I have written created several extension method classes, which can now be used from the .feature files. I think this may be the best way forward for this project, considering that implementing a unit test framework is not really what we need when testing physical machines.

I have setup VS code for our test creators, so that they can write proper gherkin with a linter (I think is the correct word), without the need of knowing any of the C# implementation. Furthermore I have added a prebuild event for my .csproj, which will compile the .feature to .feature.g.cs, which are then automatically included in my project and are resolved via reflection.

An example of what we currently have:

Test1.feature

Feature: FirstTest

# ShortName TheShortNameOrSomething
# DisplayName MyAmazingName

Scenario: Test some random value
    Given the setup is in base state
    When the register 1234 is set to value 5678
    And we wait for 23 seconds
    Then the register 2345 should be between 123 and 456 mA

TestStepExtensions.cs

//using TechTalk.SpecFlow;

using ProductionTooling.DataTypes.Attributes;
using ProductionTooling.DataTypes.Interfaces;
using ProductionTooling.Model.Abstractions;
using ProductionTooling.Model.TestResults;

namespace ProductionTooling.Model.StepDefinitions;

[StepDescription()]
public static class SomeDefinitionStepDefinitions
{
    [Given("the setup is in base state")]
    public static async Task GivenTheSetupIsInBaseState(this TestStep testStep)
    {
        IHardwareManager hardwareManager = testStep.HardwareManager;
        await hardwareManager.Connect();
    }

    [When("the register (.*) is set to value (.*)")]
    public static async Task WhenRegisterIsSetToValue(this TestStep testStep, int register, int newRegisterValue)
    {
        IHardwareManager hardwareManager = testStep.HardwareManager;
        await hardwareManager.SetRegisterValue(register, newRegisterValue);
    }

    [When("we wait for (.*) seconds")]
    public static async Task WhenWeWaitForSeconds(this TestStep testStep, int waitInNumberOfSeconds)
    {
        await Task.Delay(TimeSpan.FromSeconds(waitInNumberOfSeconds));
    }

    [Then("the register (.*) should be between (.*) and (.*) (.*)")]
    public static async Task ThenRegisterValueBetween(this TestStep testStep, int register, int minValue, int maxValue, string units)
    {
        IHardwareManager hardwareManager = testStep.HardwareManager;
        double registerValue = await hardwareManager.GetRegisterValue(register);
        testStep.TestResults.Add(new ValueResult(testStep.TestManager, "someName", "SomeShortName", "", registerValue, units, minValue, maxValue));
    }
}

This is then compiled to Test1.feature.g.cs:

 // Auto-generated code
using ProductionTooling.Model;
using ProductionTooling.Model.Abstractions;
using ProductionTooling.Model.Enums;
using ProductionTooling.Model.TestResults;
using ProductionTooling.Model.StepDefinitions;

namespace ProductionTooling.Model.Tests.FirstTest;

public class FirstTestStep2 : TestStep
{
    public FirstTestStep2(IProductionToolingTestManager testManager)
        : base(testManager)
    {
        this.DisplayName = "MyAmazingName";
        this.ShortName = "TheShortNameOrSomething";
    }

    public async override Task Execute(EButtonOptions buttonOptions)
    {
        #region Given
        await this.GivenTheSetupIsInBaseState();
        #endregion Given

        #region When
        await this.WhenRegisterIsSetToValue(1234,5678);
        await this.WhenWeWaitForSeconds(23);
        #endregion When

        #region Then
        await this.ThenRegisterValueBetween(2345,123,456,"mA");
        #endregion Then
    }
}

I'm not sure what the limits of this approach are, we will probably run into some problems, but this feels like the way to go for now.

Related