如何在测试期间伪造Startup.cs中声明的服务?

时间:2017-10-16 10:56:06

标签: c# testing asp.net-core autofac

我想为我的Asp .net核心应用程序编写集成测试,但我不希望我的测试使用某些服务的实际实现。

public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        ...
        services.AddTransient<IExternalService,ExternalService>();
        ...
    }
    public void Configure(IApplicationBuilder app, IHostingEnvironment env)
    {
        ...
    }
}

public interface IExternalService
{
    bool Verify(int id);
}

public class ExternalService : IExternalService
{
    public bool Verify(int id)
    {
        //Implemetation is here.
        //I want to fake this implemetation during testing.
    }
}

[Fact]
public void TestCase()
{
    //Stub out service
    var myExtService = new Mock<IExternalService>();

    //Setup response by stub
    myExtService
        .Setup(p => p.Verify(It.IsAny<int>()))
        .Returns(false);

    var host = new WebHostBuilder()
        .UseStartup<Startup>()
        .ConfigureServices((services) =>
        {
            //Setup injection
            services.AddTransient<IExternalService>((a) =>
            {
                return myExtService.Object;
            });
        });

    var server = new TestServer(host);

    var client = server.CreateClient();

    var response = client.GetAsync("/").Result;

    var responseString = response.Content.ReadAsStringAsync().Result;

    Assert.Contains("Your service returned: False", responseString);
}

测试用例中的当前注入设置不起作用,因为通过模拟注入了ExternalService。

但是,当我从services.AddTransient<IExternalService,ExternalService>;移除Startup时,测试将会通过。

很可能稍后调用Startup中的那个,并且应用程序首选该类中的所有设置。

我有哪些选项可以在测试中设置一些依赖项,但是使用Startup中声明的所有其他内容?

更新

  1. 应用程序代码应该不知道测试。
  2. 测试应注意:
    • (弱类型)端点 - 如果此更改则测试失败
    • IExternalService接口
  3. 测试不应该关心应用程序是否具有剃刀页面或使用mvc或应用程序如何在端点和IExternalService之间连接。
  4. 测试不应该设置或配置应用程序(除了存根IExternalService)以使其工作。
    • 我知道仍然需要创建WebHostBuilder,但我的观点是在测试用例中配置应该是最小的,并且仍然应该在应用程序端描述大部分配置。

3 个答案:

答案 0 :(得分:1)

我所知道的唯一选择是使用WebHostBuilder设置UseEnvironment

var host = new WebHostBuilder()
            .UseStartup<Startup>()
            .ConfigureServices(services =>
            {
                //Setup injection
                services.AddTransient<IExternalService>(provider =>
                {
                    return myExtService.Object;
                });
            })
            .UseEnvironment("IntegrationTest");

然后在ConfigureServices中的Startup方法中添加条件:

public void ConfigureServices(IServiceCollection services)
    {
        if (Configuration["Environment"] != "IntegrationTest")
        {
            services.AddTransient<IExternalService, ExternalService>();
        }

        services.AddMvc();

        // ...
    }

<强>更新

我做了一些更多的讨论,另一个选择是不使用UseStartup扩展方法,而是直接配置WebHostBuilder。您可以通过多种方式执行此操作,但我认为您可以创建自己的扩展方法来在测试中创建模板:

public static class WebHostBuilderExt
{
    public static WebHostBuilder ConfigureServicesTest(this WebHostBuilder @this, Action<IServiceCollection> configureServices)
    {
        @this.ConfigureServices(services =>
            {
                configureServices(services);

                services.AddMvc();
            })
            .Configure(builder =>
            {
                builder.UseMvc();
            });
        return @this;
    }
}

现在您的测试可以设置如下:

        var host = new WebHostBuilder()
            .ConfigureServicesTest(services =>
            {
                //Setup injection
                services.AddTransient<IInternalService>(provider =>
                {
                    return myExtService.Object;
                });
            });

        var server = new TestServer(host);

这意味着您必须显式设置容器将为您调用的特定端点解析的所有实现。您可以选择模拟或使用具体实现。

答案 1 :(得分:1)

经过数小时的研究后,我找到了解决方案。

我找不到单独使用内置依赖注入解决方案的方法,因此我选择了第三方DI解决方案 - Autofac

想法是使用WebHostBuilder(声明主程序)并添加必要的选项,以便在测试期间伪造一些服务。

我学到的东西:

  • 如果您使用“{1}}启动”,则会在host.UseStartup<Startup>
  • 之后创建
  • 你不能像host.ConfigureServices()
  • 那样向Startup注入一些东西
  • 但是,如果您已在host.UseStartup<Startup>(new Dependency())中注册了相关性,那么它将在创建启动之前解析,并使用构造函数host.ConfigureServices(services => services.AddTransient<IDependency, MyDependency>())创建public Startup(IDependency dependency)

我的申请方:

Startup

测试用例:

public class Program
{
    public static void Main(string[] args)
    {
        CreateWebHost(args)
           .Build()
           .Run();
    }

    public static IWebHostBuilder CreateWebHost(string[] args) =>
        WebHost.CreateDefaultBuilder(args)
            .ConfigureServices((services) =>
            {
                //Setup autofac.
                services.AddAutofac();

                //Register module dependency that Startup requires.
                services.AddTransient<Module, MyAutofacModule>();

                ////It would a bit cleaner to use autofac to setup Startup dependency,
                ////but dependency did not get resolved for Startup.
                //services.AddAutofac((builder) =>
                //{
                //   builder.RegisterModule(new AutofacModule());
                //});
            })
            .UseStartup<Startup>();
}

public class MyAutofacModule : Module
{
    protected override void Load(ContainerBuilder builder)
    {
        //Register all application dependencies in this module.
        builder.Register((c) => new ExternalService()).As<IExternalService>();
    }
}

public class Startup
{
    private Module applicationDIModule;

    public Startup(Module applicationDIModule)
    {
        this.applicationDIModule = applicationDIModule;
    }

    public void ConfigureServices(IServiceCollection services)
    {
        //We can add build-in services such as mvc and authorization,
        //but I would not use Add(Transient/Scoped/Singleton) here.
        //You should register domain specific dependecies in MyAutofacModule,
        //since it will be added after this method call.
        services.AddMvc();
    }

    //This method is called after ConfigureServices (refer to Autofac link).
    public void ConfigureContainer(ContainerBuilder builder)
    {
        //We will register injected module.
        builder.RegisterModule(applicationDIModule);
    }

    public void Configure(IApplicationBuilder app, IHostingEnvironment env)
    {
        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
        }

        app.UseMvcWithDefaultRoute();
    }
}

虽然感觉有点脆,但我仍然更喜欢这个解决方案而不是@ODawg所建议的。他的解决方案可行,但我发现在将来添加新的测试用例时会引起麻烦。

答案 2 :(得分:1)

您唯一需要更改的就是使用ConfigureTestServices而不是ConfigureServicesConfigureTestServicesStartup之后运行,因此您可以使用模拟/存根替换实际的实现。 ConfigureServices是用于此目的的较新版本,相反,它配置“主机服务”,该主机服务在应用程序的主机构建阶段使用,并复制到应用程序的DI容器中。

ConfigureTestServices在ASP Core 2.1及更高版本中可用。

var host = new WebHostBuilder()
    .UseStartup<Startup>()
    .ConfigureTestServices((services) =>
    {
        //Setup injection
        services.AddTransient<IExternalService>((a) =>
        {
            return myExtService.Object;
        });
    });