与WebApplicationFactory集成测试中的替代数据库提供程序

时间:2018-10-14 03:50:48

标签: c# asp.net-core

我正在遵循用于集成测试.Net Core(https://docs.microsoft.com/en-us/aspnet/core/test/integration-tests?view=aspnetcore-2.1)的官方MS文档。

我能够在不覆盖正在测试的应用程序的启动类的情况下完成集成测试的第一部分(即,我正在使用不覆盖任何服务的Web应用程序工厂)。

我想覆盖数据库设置,以将内存数据库用于集成测试。我遇到的问题是配置继续尝试将SQL Server用于services.AddHangfire()

如何在集成测试中仅覆盖特定项目之上的内容?我只想覆盖AddHangfire设置,而不想覆盖services.AddScoped<ISendEmail, SendEmail>().任何帮助。

使用自定义Web应用程序工厂测试类

 public class HomeControllerShouldCustomFactory : IClassFixture<CustomWebApplicationFactory<Startup>>
    {
        private readonly HttpClient _client;
        private readonly CustomWebApplicationFactory<Startup> _factory;

        public HomeControllerShouldCustomFactory(CustomWebApplicationFactory<Startup> factory)
        {
            _factory = factory;
            _client = factory.CreateClient();
        }

        [Fact]
        public async Task IndexRendersCorrectTitle()
        {
            var response = await _client.GetAsync("/Home/Index");

            response.EnsureSuccessStatusCode();

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

            Assert.Contains("Send Email", responseString);
        }
}

自定义Web应用程序工厂

public class CustomWebApplicationFactory<TStartup>: WebApplicationFactory<SendGridExample.Startup>
    {
        protected override void ConfigureWebHost(IWebHostBuilder builder)
        {
            builder.ConfigureServices(services =>
            {
                // Create a new service provider.
                var serviceProvider = new ServiceCollection()
                    .AddEntityFrameworkInMemoryDatabase()
                    .BuildServiceProvider();

                var inMemory = GlobalConfiguration.Configuration.UseMemoryStorage();
                services.AddHangfire(x => x.UseStorage(inMemory));

                // Build the service provider.
                var sp = services.BuildServiceProvider();

            });
        }
    }

我正在测试的应用程序中的startup.cs

public IConfiguration配置{ }     公共IHostingEnvironment环境{ }

public void ConfigureServices(IServiceCollection services)
{
    services.AddHangfire(x => x.UseSqlServerStorage(Configuration.GetConnectionString("ASP_NetPractice")));
    services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
    services.AddScoped<ISendEmail, SendEmail>();
}


public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    app.UseHangfireServer();
    app.UseHangfireDashboard();
    RecurringJob.AddOrUpdate<ISendEmail>((email) => email.SendReminder(), Cron.Daily);
    app.UseMvc();

更新

在我仅使用实体框架的其他示例项目中,我没有看到此问题。我有一个使用SQL Server的应用程序数据库上下文的简单应用程序。在测试类中,我使用内存数据库覆盖了它,一切正常。我不知道为什么它将在我的示例应用程序中起作用而在我的主应用程序中不起作用。这与HangFire的工作方式有关吗?

在我的测试应用程序(下面的示例代码)中,我可以删除我的sql数据库,运行我的测试,并且测试通过,因为应用程序数据库上下文不会去寻找sql服务器实例,而是使用内存数据库。在我的应用程序中,HangFire服务一直尝试使用sql服务器数据库(如果我删除数据库并尝试使用内存数据库进行测试-它将失败,因为它找不到其尝试连接的实例) 。当两个项目都使用相似的路径时,两个项目的工作方式为何会有如此巨大的差异?

我为集成测试运行了调试器,该调试器在上面的Home控制器上调用了index方法(使用CustomWebApplicationFactory)。在初始化测试服务器时,它将经历我的启动类,该类在下面的ConfigureServices中调用:

services.AddHangfire(x => x.UseSqlServerStorage(Configuration.GetConnectionString("ASP_NetPractice"))); 

然后,Configure方法尝试调用以下语句:

app.UseHangfireServer();

这时测试失败,因为它找不到数据库。该数据库托管在Azure上,因此我尝试将其替换为内存服务器以进行某些集成测试。我采用的方法不正确吗?


我的示例应用程序可以正常工作

示例应用程序中的应用程序数据库上下文

public class ApplicationDbContext : DbContext
    {
        public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
            : base(options)
        {
        }

        public virtual DbSet<Message> Messages { get; set; }

        public async  Task<List<Message>> GetMessagesAsync()
        {
            return await Messages
                .OrderBy(message => message.Text)
                .AsNoTracking()
                .ToListAsync();
        }

        public void Initialize()
        {
            Messages.AddRange(GetSeedingMessages());
            SaveChanges();
        }

        public static List<Message> GetSeedingMessages()
        {
            return new List<Message>()
            {
                new Message(){ Text = "You're standing on my scarf." },
                new Message(){ Text = "Would you like a jelly baby?" },
                new Message(){ Text = "To the rational mind, nothing is inexplicable; only unexplained." }
            };
        }
    }

示例应用程序中的Startup.cs

services.AddDbContext<ApplicationDbContext>(options =>
                options.UseSqlServer(
                    Configuration.GetConnectionString("DefaultConnection")));

CustomWebApplicationFactory-在我的单元测试项目中

public class CustomWebApplicationFactory<TStartup>
     : WebApplicationFactory<Startup>
    {
        protected override void ConfigureWebHost(IWebHostBuilder builder)
        {
            builder.ConfigureServices(services =>
            {
                // Create a new service provider.
                var serviceProvider = new ServiceCollection()
                    .AddEntityFrameworkInMemoryDatabase()
                    .BuildServiceProvider();

                // Add a database context (ApplicationDbContext) using an in-memory 
                // database for testing.
                services.AddDbContext<ApplicationDbContext>(options =>
                {
                    options.UseInMemoryDatabase("InMemoryDbForTesting");
                    options.UseInternalServiceProvider(serviceProvider);
                });

                // Build the service provider.
                var sp = services.BuildServiceProvider();

            });
        }
    }

我的单元测试项目中的单元测试

public class UnitTest1 : IClassFixture<CustomWebApplicationFactory<Startup>>
    {
        private readonly HttpClient _client;
        private readonly CustomWebApplicationFactory<Startup> _factory;

        public UnitTest1(CustomWebApplicationFactory<Startup> factory)
        {
            _factory = factory;
            _client = factory.CreateClient();
        }


        [Fact]
        public async System.Threading.Tasks.Task Test1Async()
        {
            var response = await _client.GetAsync("/");

            //response.EnsureSuccessStatusCode();

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

            Assert.Contains("Home", responseString);
        }

enter image description here


更新2

我想我找到了一种替代方案,可以尝试覆盖我的集成测试类中的所有配置。由于覆盖HangFire和ApplicationDBContext相比要复杂得多,因此我想出了以下方法:

Startup.cs

    if (Environment.IsDevelopment())
    {
        var inMemory = GlobalConfiguration.Configuration.UseMemoryStorage();
        services.AddHangfire(x => x.UseStorage(inMemory));
    }
    else
    {
        services.AddHangfire(x => x.UseSqlServerStorage(Configuration["DBConnection"]));

    }

然后在我的CustomWebApplicationBuilder中,我覆盖环境类型以进行测试:

public class CustomWebApplicationFactory<TStartup> : WebApplicationFactory<SendGridExample.Startup>
    {
        protected override void ConfigureWebHost(IWebHostBuilder builder)
        {
            builder.UseEnvironment("Development"); //change to Production for alternate test
            builder.ConfigureServices(services =>
            {
                // Create a new service provider.
                var serviceProvider = new ServiceCollection()
                    .AddEntityFrameworkInMemoryDatabase()
                    .BuildServiceProvider();
           });
        }

    }

使用这种方法,我不必担心必须执行额外的逻辑来满足对活动DB进行hangfire的检查。它可以工作,但是我在生产启动类中引入分支时,我不是100%相信它的最佳方法。

1 个答案:

答案 0 :(得分:1)

您需要检查两种不同的情况。

  1. 按课程BackgroundJob
  2. 创建工作
  3. 通过界面IBackgroundJobClient创建工作

对于第一种选择,您无法将SqlServerStorage替换为MemoryStorage

对于UseSqlServerStorage,它将通过JobStorage重设SqlServerStorage

        public static IGlobalConfiguration<SqlServerStorage> UseSqlServerStorage(
        [NotNull] this IGlobalConfiguration configuration,
        [NotNull] string nameOrConnectionString)
    {
        if (configuration == null) throw new ArgumentNullException(nameof(configuration));
        if (nameOrConnectionString == null) throw new ArgumentNullException(nameof(nameOrConnectionString));

        var storage = new SqlServerStorage(nameOrConnectionString);
        return configuration.UseStorage(storage);
    }

UseStorage

    public static class GlobalConfigurationExtensions
{
    public static IGlobalConfiguration<TStorage> UseStorage<TStorage>(
        [NotNull] this IGlobalConfiguration configuration,
        [NotNull] TStorage storage)
        where TStorage : JobStorage
    {
        if (configuration == null) throw new ArgumentNullException(nameof(configuration));
        if (storage == null) throw new ArgumentNullException(nameof(storage));

        return configuration.Use(storage, x => JobStorage.Current = x);
    }

这意味着,无论您在CustomWebApplicationFactory中进行什么设置,UseSqlServerStorage都将BackgroundJob重置为SqlServerStorage

对于第二种选择,它可以将

IBackgroundJobClient替换为MemoryStorage
    public class CustomWebApplicationFactory<TEntryPoint> : WebApplicationFactory<Startup>
{
    protected override void ConfigureWebHost(IWebHostBuilder builder)
    {
        builder.ConfigureServices(services =>
        {
            services.AddSingleton<JobStorage>(x =>
            {
                return GlobalConfiguration.Configuration.UseMemoryStorage();
            });
        });
    }
}

最后,我建议您注册IBackgroundJobClient并尝试使用第二种方法来满足您的要求。

更新1

对于数据库不可用,无法通过配置依赖项注入来解决。该错误是由调用services.AddHangfire(x => x.UseSqlServerStorage(Configuration.GetConnectionString("ASP_NetPractice")));引起的。

为解决此错误,您需要在Startup.cs中覆盖此代码。

尝试以下步骤:

  • Startup更改为以下内容:

    public class Startup
    {
    public Startup(IConfiguration configuration)
    {
        Configuration = configuration;
    }
    
    public IConfiguration Configuration { get; }
    
    // This method gets called by the runtime. Use this method to add services to the container.
    public void ConfigureServices(IServiceCollection services)
    {
        //Rest Code
    
        ConfigureHangfire(services);
    }
    
    // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
    public void Configure(IApplicationBuilder app, IHostingEnvironment env)
    {
        //Rest Code
        app.UseHangfireServer();
        RecurringJob.AddOrUpdate(() => Console.WriteLine("RecurringJob!"), Cron.Minutely);
    
    }
    
    protected virtual void ConfigureHangfire(IServiceCollection services)
    {
        services.AddHangfire(config =>
          config.UseSqlServerStorage(Configuration.GetConnectionString("HangfireConnection"))
        );
    }
    }
    
  • 在测试项目中创建StartupTest

    public class StartupTest : Startup
    {
    public StartupTest(IConfiguration configuration) :base(configuration)
    {
    
    }
    protected override void ConfigureHangfire(IServiceCollection services)
    {
        services.AddHangfire(x => x.UseMemoryStorage());
    }
    }
    
  • CustomWebApplicationFactory

    public class CustomWebApplicationFactory<TEntryPoint> : WebApplicationFactory<TEntryPoint> where TEntryPoint: class
    {
    protected override IWebHostBuilder CreateWebHostBuilder()
    {
        return WebHost.CreateDefaultBuilder(null)
            .UseStartup<TEntryPoint>();
    }
    }
    
  • 测试

    public class HangfireStorageStartupTest : IClassFixture<CustomWebApplicationFactory<StartupTest>>
    {
    private readonly HttpClient _client;
    private readonly CustomWebApplicationFactory<StartupTest> _factory;
    
    public HangfireStorageStartupTest(CustomWebApplicationFactory<StartupTest> factory)
    {
    
        _factory = factory;
        _client = factory.CreateClient();
    }
    }