Zwei Tage im Zeichen der Integration auf der...

Read more

Zwei Tage im Zeichen der Integration auf der...

Read more

Ein knackiger und kompakter Überblick auf die...

Read more

Auch am dritten Tag gab es interessante...

Read more

Die großen Ankündigungen aus dem ersten Tag...

Read more

Keynote - Stand der Integration - Jon Fancey:...

Read more

Integrieren Sie noch oder managen Sie bereits den...

Read more

Inwieweit lassen sich vorhandene BizTalk Lösungen...

Read more

Wie kann ein Prozess Monitoring einfach und...

Read more

Führende Veranstaltung rund um Connectivity auf...

Read more

How-to: SpecFlow Tags’n’Targets

Was ist SpecFlow? SpecFlow ist ein Testing Framework für BDD (Behaviour Driven Development) in .NET. Mit SpecFlow ist es möglich mit Hilfe von Gherkin ganz einfach lesbare Tests zu schreiben.

 

SpecFlow ist ein Testing Framework für BDD (Behaviour Driven Development) in .NET. Mit SpecFlow ist es möglich mit Hilfe von Gherkinganz einfach lesbare Tests zu schreiben.

Scenario: Lithium Ion Battery Full
  Given a LithiumIon battery
  When this battery has a voltage of 4.2
  Then the state of charge should be full


Diese Tests sollten keinerlei Informationen über das System enthalten und beschreiben lediglich Verhalten, was getestet werden soll.
Da hinter diesem Verhalten, die Implementierung verborgen bleiben soll, sollte das auch für den dahinterstehenden Code gelten. Weswegen sich Driver als Lösung dieses Problems herauskristallisiert haben.
Driver sollen das Zielsystem abstrahieren und es ermöglichen die Zielsysteme auszutauschen.

Mehr dazu hier: Driver Pattern

Wenn man nun mehrere Driver gegen unterschiedliche Zielsysteme implementiert ist es einfach möglich die Driver auszutauschen, um ein anderes Zielsystem anzusprechen.

In den folgenden Beispielen werden die beiden Zielsysteme RestInterface und InMemory verwendet. Das RestInterface Zielsystem spricht ein System an, welches mit einer REST Schnittstelle angesprochen werden kann. Die Tests stellen dann End2End Tests dar. Bei dem InMemory Zielsystem handelt es sich um direkten Zugriff auf die Klassen und stellt UnitTests dar. Die können beide dann mit derselben Formulierung ausgeführt werden. Wenn sich das Verhalten ändern soll, werden sowohl die UnitTests als auch die End2End Tests gleichzeitig angepasst. Das Verhalten soll bei beiden Zielsystemen gleich sein und sich nicht unterscheiden.

Einige Szenarien können jedoch nicht gegen alle Treiber ausgeführt werden, weswegen es die Tags gibt.

Für das weitere Verständnis sollte die SpecFlow Dependency Injection und das Arbeiten mit SpecFlow bekannt sein.

Tags

Mit Tags können Tests mit Hilfe des dotnet test Befehls gefiltert werden.

Die Tags werden in Gherkin mit einem @ (At) über die Szenarios und Features geschrieben.

@RestInterface
Scenario: Lithium Ion Battery Emtpy
  Given a LithiumIon battery
  When this battery has a voltage of 2.8
  Then the state of charge should be empty  

@InMemory
Scenario: Lithium Ion Battery Full
  Given a LithiumIon battery
  When this battery has a voltage of 4.2
  Then the state of charge should be full

Um mit Befehl nur die Szenarios/Features auszuführen, muss man den Parameter --filter zum Testbefehl hinzufügen. Der Filter kann noch mehr, aber das sollte für das Vorgehen erstmal reichen

Es ist bei unterschiedlichen Testrunnern ein leicht anderer Filter notwendig. Hier die Standard Testrunner und deren Befehle:

Testrunner --> Filter

xUnit  --filter "Category=RestInterface"
SpecFlow+ Runner  --filter "TestCategory=RestInterface"
NUnit   --filter "TestCategory=RestInterface" oder --filter "Category=RestInterface"
MSUnit  --filter "TestCategory=RestInterface"

Der vollständige Befehl sieht dann so aus:

dotnet test –filter "Category=RestInterface"

Bei dem Befehl werden nur die Szenarios und Features ausgeführt, die den Tag @RestInterface tragen.

Bedeutung der Tags für Specflow

In Specflow haben die Tags, abgesehen vom Filtern, noch eine weitere Bedeutung. Die Tags können für die Szenarios und Features zusätzliche Hooks ausführen, falls ein Tag drübersteht.

[BeforeScenario("InMemory")]
private void ChooseLocalDriver(ScenarioContext scenarioContext)
{    
    scenarioContext.ScenarioContainer
        .RegisterTypeAs<BatteryStateOfChargeCalculator, IBatteryStateOfChargeDriver>();
}

So wird vor jedem Szenario mit dem Tag @InMemory jeweils der Hook ausgeführt und es kann in den darauffolgenden Hooks und Steps auf einen IBatteryStateOfChargeDriver zugegriffen werden.

Dieser Hook wird dann nur ausgeführt, wenn das Szenario auch genau diesen Tag hat. Sollte ein Szenario ausgeführt werden, welches nicht diesen Tag hat, so wird der Hook nicht ausgeführt.

Ein Szenario oder Feature kann mehrere Tags gleichzeitig besitzen, um mehrere Hooks auszuführen.

Targets

Wenn ein Szenario oder Feature anhand des Filters ausgewählt und gestartet wurde, so werden alle Hooks zu dem Tag ausgeführt. Nun ist es aber möglich, dass ein Szenario oder Feature mehrere Tags hat und somit auch unterschiedliche Hooks ausgeführt werden.

@RestInterface
@InMemory
Scenario: Lithium Ion Battery Full
  Given a LithiumIon battery
  When this battery has a voltage of 4.2
  Then the state of charge should be full

[BeforeScenario("InMemory")]
private void ChooseLocalDriver(ScenarioContext scenarioContext)
{    
    scenarioContext.ScenarioContainer

        .RegisterTypeAs<BatteryStateOfChargeCalculatorIBatteryStateOfChargeDriver>();
}

 

[BeforeScenario("RestInterface")]
private void ChooseRestInterfaceDriver(IConfiguration configurationScenarioContext scenarioContext)
{    
    var azureConfig = configuration.GetSection(RestConfigurationKey)

        .Get<RestTestConfiguration>();

    scenarioContext.ScenarioContainer.RegisterInstanceAs(azureConfig);
    scenarioContext.ScenarioContainer

      .RegisterTypeAs<StateOfChargeRestDriverIBatteryStateOfChargeDriver>();
}

Und hier könnte einem schon das Problem auffallen. Obwohl der Befehl dotnet test --filter ”Category=InMemory” ausgeführt wurde, so werden dennoch beide Hooks ausgeführt und das könnte nicht gewollt sein.

Eine Lösung für das Problem ist die Einführung von Execution Targets, welches bestimmt, auf welchem Zielsystem die Tests ausgeführt werden soll.

Dazu muss eine Konfigurationsdatei erstellt werden, die das Target bestimmt. Diese Konfigurationsdatei muss ausgelesen werden und in die Dependency Injection von SpecFlow registriert werden.

{
  "Target""InMemory" 

Jetzt muss kann mit einer weiteren Hook eine TargetConfiguration in die Dependency Injection registriert werden. Das sieht dann folgendermaßen aus:

[Binding]
public class TargetConfigurationHook
{
    public const string ConfigurationTargetKey = "Target";

    [BeforeScenario]
    private void GetTargetConfiguration(
        IConfiguration configuration
        ScenarioContext scenarioContext
    )

    {
        var target = configuration.GetValue<string>(ConfigurationTargetKey);
        scenarioContext.ScenarioContainer
            .RegisterInstanceAs(new TargetConfiguration(target));

    }
}

public class TargetConfiguration
{
    private readonly string _currentTarget;
    public static string RestInterface = "RestInterface";
    public static string InMemory = "InMemory";
    

    public TargetConfiguration(string currentTarget)
    {
        if (!ExistsTarget(currentTarget))

        {
            throw new InvalidOperationException("Target does not exists.");

        }
                _currentTarget = currentTarget;

    }

 

    private static bool ExistsTarget(string currentTarget)
    {
        return
            currentTarget == RestInterface ||
            currentTarget == InMemory;

    }


    public bool Is(string target)
    {
        return _currentTarget == target;

    }
}

Danach kann in den Hooks das gewünschte Target überprüft werden und wenn man sich nicht im gewünschten Target befindet, wird die Ausführung abgebrochen.

[BeforeScenario("RestInterface")]
public void ChooseRestDriver(IConfiguration configurationTargetConfiguration targetScenarioContext scenarioContext)
{
    if (!target.Is(TargetConfiguration.RestInterface))

    {
        return;

    }

    

    var azureConfig = configuration.GetSection(RestConfigurationKey)
        .Get<RestTestConfiguration>();

    scenarioContext.ScenarioContainer.RegisterInstanceAs(azureConfig);
    scenarioContext.ScenarioContainer

      .RegisterTypeAs<StateOfChargeRestDriverIBatteryStateOfChargeDriver>();
}
 

Damit nun die Target Tags nicht mit den sonstigen Tags verwechselt werden können bekommen die Target Tags das Präfix „Target:“.

In der Feature Datei:

@Target:RestInterface
Scenario: Lithium Ion Battery Emtpy
  Given a LithiumIon battery
  When this battery has a voltage of 2.8
  Then the state of charge should be empty

 

In den Hooks:

[BeforeScenario("Target:RestInterface")]
public void ChooseRestDriver(IConfiguration configurationTargetConfiguration targetScenarioContext scenarioContext)
{
    if (!target.Is(TargetConfiguration.RestInterface))

    {
        return;

    }

    

    var azureConfig = configuration.GetSection(RestConfigurationKey)
        .Get<RestTestConfiguration>();

    scenarioContext.ScenarioContainer.RegisterInstanceAs(azureConfig);
    scenarioContext.ScenarioContainer

      .RegisterTypeAs<StateOfChargeRestDriverIBatteryStateOfChargeDriver>();
}

Der Testbefehl:

dotnet test –filter "Category=Target:RestInterface"

So ist es dann möglich für die unterschiedlichen Targets auch unterschiedliche Driver zu haben.

Mit folgendem Powershell Code kann der Schritt mit dem Target setzen und Test ausführen erleichtert werden:

# Setzen des Targets in der Konfigurationsdatei.
function SetTarget($Target){
    $appsettings = $(Get-Content appsettings.json | ConvertFrom-Json)
    $appsettings.Target = $Target
    $appsettings | ConvertTo-Json > .\appsettings.json

}

# Wenn keine Argumente angegeben werden wird ein Benutzungshinweis ausgegeben
if($args.Count -eq 0){
    Write-Host "Usage: powershell './RunTests.ps1' <Target> <Target>..."
    Write-Host "Example: powershell './RunTests.ps1' RestInterface InMemory"
    exit

}

# Führt alle Targets nacheinander aus.
foreach ($target in $args) {
    Write-Host "Running tests for target: $target"

    SetTarget($target)
    dotnet test --filter "Category=Target:$target"
}

Dieses Skript muss dann (wie auch im Skript zu sehen) wie folgt aufgerufen werden:

powershell './RunTests.ps1' <Target> <Target>...

Voraussetzung ist natürlich, dass die appsettings.json im aktuellen Working Directory ist.

Falls mehrere Targets angegeben werden, werden diese nacheinander ausgeführt. So können die Tests in unterschiedlichen Umgebungen gleich hintereinander ausgeführt werden.

 

© QUIBIQ GmbH · Imprint · Data protection