CONNECTED Conference 2023 - Aufzeichnungen jetzt hier verfügbar +++                     

Suche

über alle News und Events

 

Alle News

 

Messaging mit dem Service Bus ermöglicht die...

Read more

Sebastian Meyer, Microsoft & SAP...

Read more

Für Entwickler, Architekten, Projektleiter und...

Read more

In der Welt der Softwareentwicklung ist die...

Read more

QUIBIQ spendet für den guten Zweck – und für...

Read more

Eine bestimmte Antwort auf einen HTTP Request zu...

Read more

In einer Welt, die von stetigem Wandel geprägt...

Read more

In einem unserer Kundenprojekte, war das Ziel eine...

Read more

QUIBIQ Hamburg wird mit dem Hamburger...

Read more

Zwei Tage lang wurde vom 14.-15.11 wieder das...

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.

 

Ihre Kontaktmöglichkeiten

Sie haben eine konkrete Frage an uns


 

Bleiben Sie immer auf dem Laufenden


 

Mit meinem "Ja" erkläre ich mich mit der Verarbeitung meiner Daten zur Zusendung von Informationen einverstanden. Ich weiß, dass ich diese Erklärung jederzeit durch einfache Mitteilung widerrufen kann. Bei einem Nein an dieser Stelle erhalte ich zukünftig keine Informationen mehr.

© QUIBIQ GmbH · Impressum · Datenschutz