如何在 SpecFlow Step 注入物件 / Context Dependency Injection in Specflow

當 SpecFlow 的測試步驟需要依賴其他物件時,SpecFlow 支援  Context Dependency Injection,只要在 Step 的建構函數開洞,SpecFlow 就會自動幫你注入

開發環境

  • Windows 11
  • .NET 6
  • Rider 2022.2.4

環境準備

先依照以下步驟準備測試專案的環境

新增一個測試專案

dotnet new mstest --language "C#" --framework "net6.0" -o Lab.StepDependencyInjection

 

安裝套件

dotnet add package SpecFlow.MsTest --version 3.9.74

 

新增 Demo1 的 feature

Feature: Demo1

    Scenario: 測試步驟注入1
        When 取得檔案路徑
        Then 預期得到 "File Provider"

 

新增 FileProvider

public class FileProvider
{
    public string GetPath()
    {
        return "File Provider";
    }
}

 

Context Dependency Injection

SpecFlow 提供 Context Dependency Injection 的功能,讓 TechTalk.SpecFlow.Binding 物件彼此之間可以使用相同的物件,只要在建構子依賴該物件,SpecFlow 的框架就會自動幫你實例化,如下範例

[Binding]
[Scope(Feature = "Demo0")]
public class Demo0Steps
{
    private readonly FileProvider _fileProvider;

    private ScenarioContext ScenarioContext { get; }

    public Demo2Steps(ScenarioContext scenarioContext, FileProvider fileProvider)
    {
        this.ScenarioContext = scenarioContext;
        this._fileProvider = fileProvider;
    }

    [When(@"取得檔案路徑")]
    public void When取得檔案路徑()
    {
        var path = this._fileProvider.GetPath();
        this.ScenarioContext.Set(path, "actual");
    }

    [Then(@"預期得到 ""(.*)""")]
    public void Then預期得到(string expected)
    {
        var actual = this.ScenarioContext.Get<string>("actual");
        Assert.AreEqual(expected, actual);
    }
}

 

若物件有建構子,預設沒有辦法幫你自動實例化

Step 有建構子,實作 TechTalk.SpecFlow.Steps,相關的物件都不會被建立,有需要使用 ScenarioContext、ScenarioStepContext、FeatureContext 需要自行在建構函數開洞

 

IObjectContainer

如果物件初始化的方式很複雜,SpecFlow 也有提供 IObjectContainer(DI Container),有四種註冊方式,使用方式也不難

  • IStrategyRegistration RegisterTypeAs<TType, TInterface>(string name = null) where TType : class, TInterface;
  • void RegisterInstanceAs<TInterface>(TInterface instance, string name = null, bool dispose = false) where TInterface : class;
  • void RegisterInstanceAs(object instance, Type interfaceType, string name = null, bool dispose = false);
  • IStrategyRegistration RegisterFactoryAs<TInterface>(
      Func<IObjectContainer, TInterface> factoryDelegate,
      string name = null);

四種解析物件

  • T Resolve<T>();
  • T Resolve<T>(string name);
  • object Resolve(Type typeToResolve, string name = null);
  • IEnumerable<T> ResolveAll<T>() where T : class;

 

簡單範例如下

註冊:objectContainer.RegisterInstanceAs(new FileProvider(), nameof(FileProvider));

取得:var target = this.objectContainer.Resolve<FileProvider>(nameof(FileProvider));

[Binding]
[Scope(Feature = "Demo1")]
public class Demo1Steps : TechTalk.SpecFlow.Steps
{
    private readonly IObjectContainer objectContainer;

    private ScenarioContext ScenarioContext { get; }

    public Demo1Steps(IObjectContainer objectContainer,
        ScenarioContext scenarioContext)
    {
        objectContainer.RegisterInstanceAs(new FileProvider(), nameof(FileProvider));
        this.objectContainer = objectContainer;
        this.ScenarioContext = scenarioContext;
    }

    [When(@"取得檔案路徑")]
    public void When取得檔案路徑()
    {
        var target = this.objectContainer.Resolve<FileProvider>(nameof(FileProvider));
        var path = target.GetPath();
        this.ScenarioContext.Set(path, "actual");
    }

    [Then(@"預期得到 ""(.*)""")]
    public void Then預期得到(string expected)
    {
        var actual = this.ScenarioContext.Get<string>("actual");
        Assert.AreEqual(expected, actual);
    }
}

 

SpecFlow.DependencyInjection

除了官方內建的 DI Container,還有一些第三方的 DI Container,如果你想要創造自己的 DI Container 那可以參考 SpecFlow.Autofac

 

支援 Microsoft.Extensions.DependencyInjection 的 DI Container,也是基於 SpecFlow.Autofac 發展而來的 SpecFlow.DependencyInjection

安裝

dotnet add package SolidToken.SpecFlow.DependencyInjection --version 3.9.3

 

使用方法很簡單,在 SpecFlow 專案中創建一個回傳 Microsoft.Extensions.DependencyInjection.IServiceCollection 的靜態方法,並掛上 [ScenarioDependencies] Attribute,這樣 SpecFlow 所綁定的步驟就可以使用了

[ScenarioDependencies]
public static IServiceCollection CreateServices()
{
    var services = new ServiceCollection();
    services.AddSingleton<FileProvider>();
    return services;
}

 

範例如下:

public class SysFileProvider
{
    private readonly string _name;

    public SysFileProvider(string name)
    {
        this._name = name;
    }

    public string GetPath()
    {
        return $"{this._name}:SysFileProvider";
    }
}
[Binding]
[Scope(Feature = "Demo2")]
public class Demo2Steps
{
    private readonly SysFileProvider _sysFileProvider;

    public Demo2Steps(SysFileProvider sysFileProvider, ScenarioContext scenarioContext)
    {
        this._sysFileProvider = sysFileProvider;
        this.ScenarioContext = scenarioContext;
    }

    private ScenarioContext ScenarioContext { get; }


    [ScenarioDependencies]
    public static IServiceCollection CreateServices()
    {
        var services = new ServiceCollection();
        services.AddSingleton(new SysFileProvider("fake provider"));
        return services;
    }

    [When(@"取得檔案路徑")]
    public void When取得檔案路徑()
    {
        var path = this._sysFileProvider.GetPath();
        this.ScenarioContext.Set(path, "actual");
    }

    [Then(@"預期得到 ""(.*)""")]
    public void Then預期得到(string expected)
    {
        var actual = this.ScenarioContext.Get<string>("actual");
        Assert.AreEqual(expected, actual);
    }
}

 

要注意的是,如果使用 SolidToken.SpecFlow.DependencyInjection,所的注入行為都會統一,原本 SpecFlow 預設的注入行為就會失效。

參考

Context Injection — documentation (specflow.org)

範例位置

sample.dotblog/Test/Specflow3/Lab.StepDependencyInjection at master · yaochangyu/sample.dotblog (github.com)

若有謬誤,煩請告知,新手發帖請多包涵


Microsoft MVP Award 2010~2017 C# 第四季
Microsoft MVP Award 2018~2022 .NET

Image result for microsoft+mvp+logo