[ASP.NET Web API 2] Web API OWIN Hosting 依照測試案例,使用 Autofac 注入不同依賴物件

上一篇使用 [Unit Test] 小技巧-利用 Header 提高 Web API 可測試性,這篇的作法會讓被測目標物的職責變多,我們可以改用 Autofac 來改善這問題

開發環境

  • VS 2019
  • .NET Framework 4.7.2

新增一個單元測試專案,名為Server.UnitTest,從 Nuget 安裝以下套件

Install-Package Microsoft.AspNet.WebApi.OwinSelfHost -Version 5.2.7
Install-Package Microsoft.Owin.Diagnostics -Version 4.0.1
Install-Package Microsoft.Owin.Host.SystemWeb -Version 4.0.1
Install-Package Autofac -Version 4.9.4
Install-Package Autofac.WebApi2 -Version 4.3.0
Install-Package NSubstitute -Version 4.2.1

 

使用 Autofac 注入

Repository 如下

public interface IProductRepository
{
    string GetName();
}
 
public class ProductRepository : IProductRepository
{
    public string GetName()
    {
        return "Product";
    }
}
 
public class Product2Repository : IProductRepository
{
    public string GetName()
    {
        return "Product2";
    }
}

 

在 DefaultController 的建構函數依賴  IProductRepository。

預設的情況下 ApiController 還要有一個無參數的建構函數,不然它會無法被建立,因為等一下要用 Autofac 注入建構函數,所以先寫這樣就好。

public class DefaultController : ApiController
{
    public IProductRepository ProductRepository { get; set; }
 
    public DefaultController(IProductRepository productRepository)
    {
        this.ProductRepository = productRepository;
    }
 
    // GET api/default/
    public string Get()
    {
        return this.ProductRepository.GetName();
    }
}

https://github.com/yaochangyu/sample.dotblog/blob/master/WebAPI/Lab.AutofacDI/Server.UnitTest/Controllers/DefaultController.cs

 

Autofac 設定

  • 註冊所有 ApiController:
    builder.RegisterApiControllers(assembly);
  • 掃描組件內實作 IProductRepository 的物件:
    builder.RegisterAssemblyTypes(assembly)
        .As<IProductRepository>()
        .AsImplementedInterfaces()
        .Keyed<IProductRepository>(k => k.Name);
  • HttpConfiguration 使用 Autofac 的 DependencyResolver:
    var dependencyResolver = new AutofacWebApiDependencyResolver(container);
    this.HttpConfig.DependencyResolver = dependencyResolver;
public class AutofacManager
{
    private readonly HttpConfiguration HttpConfig;
    private          ContainerBuilder  Builder;
 
    private IContainer Container;
 
    public AutofacManager(HttpConfiguration httpConfig)
    {
        this.HttpConfig = httpConfig;
    }
 
    public ContainerBuilder CreateApiBuilder()
    {
        this.Builder = new ContainerBuilder();
        var builder = this.Builder;
 
        var assembly = Assembly.GetExecutingAssembly();
        builder.RegisterApiControllers(assembly);
 
        builder.RegisterAssemblyTypes(assembly)
               .As<IProductRepository>()
               .AsImplementedInterfaces()
               .Keyed<IProductRepository>(k => k.Name);
        return builder;
    }
 
    public IContainer CreateContainer(ContainerBuilder builder)
    {
        this.Container = builder.Build();
        var container          = this.Container;
        var dependencyResolver = new AutofacWebApiDependencyResolver(container);
        this.HttpConfig.DependencyResolver = dependencyResolver;
 
        return container;
    }
}

https://github.com/yaochangyu/sample.dotblog/blob/master/WebAPI/Lab.AutofacDI/Server.UnitTest/AutofacManager.cs

 

Startup.Configuration 裡面設定 Route 以及 Autofac

public class Startup
{
    public static AutofacManager AutofacManager { get; internal set; }
 
    public void Configuration(IAppBuilder app)
    {
        var config = new HttpConfiguration();
 
        ConfigureRoute(config);
        ConfigureAutofac(config);
        app.UseWebApi(config);
    }
 
    private static void ConfigureAutofac(HttpConfiguration config)
    {
        AutofacManager = new AutofacManager(config);
        var builder = AutofacManager.CreateApiBuilder();
        AutofacManager.CreateContainer(builder);
    }
 
    private static void ConfigureRoute(HttpConfiguration config)
    {
        config.Routes.MapHttpRoute(
                                   "DefaultApi",
                                   "api/{controller}/{id}",
                                   new {id = RouteParameter.Optional});
    }
}

https://github.com/yaochangyu/sample.dotblog/blob/master/WebAPI/Lab.AutofacDI/Server.UnitTest/Startup.cs
 

來跑個簡單的測試,看看 DefaultController 是否成功的被 Autofac 建立,成功的話會得到綠燈

不知道如何用單元測試+OWIN 測試 Web Api 請看 https://dotblogs.com.tw/yc421206/2019/01/05/webapi_test_via_owin

[TestClass]
public class UnitTest1
{
    private const  string      HOST_ADDRESS = "http://localhost:9527";
    private static IDisposable s_webApp;
    private static HttpClient  s_client;
 
    [AssemblyCleanup]
    public static void AssemblyCleanup()
    {
        s_webApp.Dispose();
    }
 
    [AssemblyInitialize]
    public static void AssemblyInitialize(TestContext context)
    {
        s_webApp = WebApp.Start<Startup>(HOST_ADDRESS);
        Console.WriteLine("Web API started!");
        s_client             = new HttpClient();
        s_client.BaseAddress = new Uri(HOST_ADDRESS);
        Console.WriteLine("HttpClient started!");
    }
 
    [TestMethod]
    public void When_Call_Get_Should_Be_Product2()
    {
        var url      = "api/Default";
        var response = s_client.GetAsync(url).Result;
        var result   = response.Content.ReadAsAsync<string>().Result;
        Assert.AreEqual("Product2", result);
    }
}

 

這個案例我用 NSub 動態建立 ProductRepository,然後替換掉原本 Autofac Container 的 ProductRepository。不會使用 NSub 有興趣的可以參考舊文章 https://dotblogs.com.tw/yc421206/series/1?qq=NSubstitute
 

  • Rebuild Container:
    SetProductRepository()
  • 註冊 ProductRepository Instance:
    builder.RegisterInstance(repository).As<IProductRepository>();
[TestMethod]
public void Given_ChangeInstance_When_Call_Get_Should_Be_FakeRepository()
{
    var fakeRepository = Substitute.For<IProductRepository>();
    fakeRepository.GetName().Returns("Fake Repository");
 
    SetProductRepository(fakeRepository);
 
    var url      = "api/Default";
    var response = s_client.GetAsync(url).Result;
    var result   = response.Content.ReadAsAsync<string>().Result;
    Assert.AreEqual("Fake Repository", result);
}
 
private static void SetProductRepository(IProductRepository repository)
{
    var autofacManager = Startup.AutofacManager;
    var builder        = autofacManager.CreateApiBuilder();
    builder.RegisterInstance(repository).As<IProductRepository>();
    autofacManager.CreateContainer(builder);
}

 

原本要使用 builder.Update(),發現這個方法已經過時,Autofac 則是建議 Rebuild。

From <https://stackoverflow.com/questions/3956505/in-autofac-how-do-i-change-the-instance-that-is-registered-after-build-has-been>

 

[SuppressMessage("Microsoft.Design", "CA1011:ConsiderPassingBaseTypesAsParameters", Justification = "You can't update any arbitrary context, only containers.")]
[Obsolete("Containers should generally be considered immutable. Register all of your dependencies before building/resolving. If you need to change the contents of a container, you technically should rebuild the container. This method may be removed in a future major release.")]
public void Update(IContainer container)
{
    Update(container, ContainerBuildOptions.None);
}

 

而且註冊的物件(Registrations )也會越來越多

 

最後,為了確保 Container 不會因為有案例變更了 ProductRepository 的實例,於是在每一次案例開始之前重建 Container

[TestInitialize]
public void TestInitialize()
{
    var autofacManager = Startup.AutofacManager;
    var builder        = autofacManager.CreateApiBuilder();
    var container = autofacManager.CreateContainer(builder);
}

 

使用 InstanceUtility 注入

同場加映,如果真的還無法接受 Autofac,咱們也可以讓測試專案能控制 DefaultController 依賴的物件,這次我把它搬到 InstanceUtility,而且只有測試專案能摸的到 InstanceUtility

public class DefaultController : ApiController
{
    // GET api/default/
    public string Get()
    {
        return InstanceUtility.ProductRepository.GetName();
    }
}
 
internal class InstanceUtility
{
    private static IProductRepository s_productRepository;
 
    public static IProductRepository ProductRepository
    {
        get
        {
            if (s_productRepository == null)
            {
                s_productRepository = new ProductRepository();
            }
 
            return s_productRepository;
        }
        set => s_productRepository = value;
    }
}

https://github.com/yaochangyu/sample.dotblog/blob/master/WebAPI/Lab.AutofacDI/Server2.UnitTest/Controllers/DefaultController.cs

 

InstanceUtility.ProductRepository 是全域使用的物件,為了滿足所有案例,開始跑測試之前,保留它的狀態,結束後還原它,下一個案例才不會壞掉

改變實例:InstanceUtility.ProductRepository = fakeRepository 

[TestMethod]
public void Given_ChangeInstance_When_Call_Get_Should_Be_FakeRepository2()
{
    var currentRepository = InstanceUtility.ProductRepository;
 
    var fakeRepository    = Substitute.For<IProductRepository>();
    fakeRepository.GetName().Returns("Fake Repository2");
 
    InstanceUtility.ProductRepository = fakeRepository;
 
    var url      = "api/Default";
    var response = s_client.GetAsync(url).Result;
    var result   = response.Content.ReadAsAsync<string>().Result;
    Assert.AreEqual("Fake Repository2", result);
 
    InstanceUtility.ProductRepository = currentRepository;
}

https://github.com/yaochangyu/sample.dotblog/blob/master/WebAPI/Lab.AutofacDI/Server2.UnitTest/UnitTest1.cs
 

用這個方法也可以輕易地改變被測目標物所依賴的物件狀態,不過要小心物件的存取範圍是 internal 還要設定 InternalsVisibleTo 唷

[assembly: InternalsVisibleTo("Server2.UnitTest")]

 

範例位置:

https://github.com/yaochangyu/sample.dotblog/tree/master/WebAPI/Lab.AutofacDI
 

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


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

Image result for microsoft+mvp+logo