当集成测试ASP.NET Core Web API和EF Core时重新配置依赖项。

29

我正在按照这个教程进行学习:
使用Entity Framework Core和SQL Server进行集成测试

我的代码如下:

集成测试类

public class ControllerRequestsShould : IDisposable
{
    private readonly TestServer _server;
    private readonly HttpClient _client;
    private readonly YourContext _context;

    public ControllerRequestsShould()
    {
        // Arrange
        var serviceProvider = new ServiceCollection()
            .AddEntityFrameworkSqlServer()
            .BuildServiceProvider();

        var builder = new DbContextOptionsBuilder<YourContext>();

        builder.UseSqlServer($"Server=(localdb)\\mssqllocaldb;Database=your_db_{Guid.NewGuid()};Trusted_Connection=True;MultipleActiveResultSets=true")
            .UseInternalServiceProvider(serviceProvider);

        _context = new YourContext(builder.Options);
        _context.Database.Migrate();

        _server = new TestServer(new WebHostBuilder()
            .UseStartup<Startup>()
            .UseEnvironment(Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT")));
        _client = _server.CreateClient();
    }

    [Fact]
    public async Task ReturnListOfObjectDtos()
    {
        // Arrange database data
        _context.ObjectDbSet.Add(new ObjectEntity{ Id = 1, Code = "PTF0001", Name = "Portfolio One" });
        _context.ObjectDbSet.Add(new ObjectEntity{ Id = 2, Code = "PTF0002", Name = "Portfolio Two" });

        // Act
        var response = await _client.GetAsync("/api/route");
        response.EnsureSuccessStatusCode();


        // Assert
        var result = Assert.IsType<OkResult>(response);            
    }

    public void Dispose()
    {
        _context.Dispose();
    }
据我了解,.UseStartUp 方法确保 TestServer 使用我的启动类。
我遇到的问题是当执行我的 Act 语句时。
var response = await _client.GetAsync("/api/route");

在我的启动类中,我遇到了连接字符串为空的错误。我认为问题是当客户端访问我的控制器时,它会注入我的数据仓库,而数据仓库又注入数据库上下文。

我觉得我需要在new WebHostBuilder部分配置服务,以便它使用测试中创建的上下文。但我不确定如何做到这一点。

Startup.cs文件中的ConfigureServices方法

        public void ConfigureServices(IServiceCollection services)
    {
        // Add framework services
        services.AddMvc(setupAction =>
        {
            setupAction.ReturnHttpNotAcceptable = true;
            setupAction.OutputFormatters.Add(new XmlDataContractSerializerOutputFormatter());
            setupAction.InputFormatters.Add(new XmlDataContractSerializerInputFormatter());
        });

        // Db context configuration
        var connectionString = Configuration["ConnectionStrings:YourConnectionString"];
        services.AddDbContext<YourContext>(options => options.UseSqlServer(connectionString));

        // Register services for dependency injection
        services.AddScoped<IYourRepository, YourRepository>();
    }
3个回答

47

@ilya-chumakov的答案太棒了。我只想再添加一种选项。

3. 使用WebHostBuilderExtensions的ConfigureTestServices方法。

方法ConfigureTestServices在Microsoft.AspNetCore.TestHost版本2.1中可用(在20.05.2018时是RC1-final)。它允许我们使用模拟对象重写现有注册。

代码:

_server = new TestServer(new WebHostBuilder()
    .UseStartup<Startup>()
    .ConfigureTestServices(services =>
    {
        services.AddTransient<IFooService, MockService>();
    })
);

7
最佳方法。此外,您可以删除某些注册,例如 services.RemoveAll(typeof(IHostedService)) - RusArt
在看到这个答案之前,我把我的依赖注入放在了测试的.ConfigureServices中。这意味着我必须在实际代码中使用.TryAddTransient而不是AddTransient。我更喜欢这种方式。 - Andrew Radford
1
自从我升级到 .net core 3.0,看起来我需要先删除之前的注册信息 :-( - Zenuka
1
最佳选项,拯救了这一天:D - Ricardo Pontual

27

以下有两个选项:

1. 使用WebHostBuilder.ConfigureServices

结合使用WebHostBuilder.ConfigureServicesWebHostBuilder.UseStartup<T>来覆盖和模拟Web应用程序的DI (Dependency Injection)注册:

_server = new TestServer(new WebHostBuilder()
    .ConfigureServices(services =>
    {
        services.AddScoped<IFooService, MockService>();
    })
    .UseStartup<Startup>()
);

public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        //use TryAdd to support mocking IFooService
        services.TryAddTransient<IFooService, FooService>();
    }
}

关键点在于在原始的Startup类中使用TryAdd方法。自定义的WebHostBuilder.ConfigureServices会在原始的Startup之前调用,因此模拟服务会在原始服务之前注册。如果相同的接口已经被注册,TryAdd不会做任何事情,因此真正的服务甚至不会被触及。
更多信息: 运行ASP.NET Core应用程序的集成测试

2. 继承 / 新的 Startup 类

创建TestStartup类以重新配置ASP.NET Core DI。您可以从Startup继承它并仅覆盖需要的方法。
public class TestStartup : Startup
{
    public TestStartup(IHostingEnvironment env) : base(env) { }

    public override void ConfigureServices(IServiceCollection services)
    {
        //mock DbContext and any other dependencies here
    }
}

或者可以从头开始创建TestStartup,以保持测试更加清晰。

并在UseStartup中指定它来运行测试服务器:

_server = new TestServer(new WebHostBuilder().UseStartup<TestStartup>());

这是一个完整的大例子:如何使用内存数据库集成测试您的asp .net core应用程序


如何覆盖 Startup.cs 中的方法 public void ConfigureServices(IServiceCollection services),如果它不是 Virtual 的? - Gaurav Arora
1
@GauravAroraa,只需将基本方法设置为“虚拟”的即可。这不会破坏任何东西。 - Ilya Chumakov
除非解决了另一个问题,否则它并没有解决这个问题。 - Gaurav Arora
1
@GauravAroraa,有什么问题吗?您只需要在TestStartup类中执行正确的代码即可。如何执行并不重要。甚至可以在基本的Startup类中传递一个变量。https://dev59.com/OqLia4cB1Zd3GeqPjG8m#44486236 - Ilya Chumakov

0
@Ilya-Chumakov @lilo.jacob,你们两个的回答都在正确的方向上,但我想再加上最后一点补充,使回答完美无缺。
如果你有
.ConfigureServices(services =>
                   {
                       serviceCollection
                      .Replace(ServiceDescriptor.Scoped(typeof(IFooService), a => MockService));
                   }
               )

如果IFooService已经被Startup添加了,那么它将被移除并替换。如果它还没有被添加,那么将会添加它。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接