Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Overriding database provider in integration test with WebApplicationFactory

Following the official MS integration testing .Net Core.

I was able to get the first part of the integration test done where I was not overriding the startup class of the application I am testing (i.e. I was using a web application factorythat did not override any services).

I want to override the database setup to use an in-memory database for the integration test. The problem I am running into is that the configuration continues to try and use the sql server for services.AddHangfire().

How do I override only above specific item in my integration test? I only want to override the AddHangfire setup and not services.AddScoped<ISendEmail, SendEmail>(). Any help would be appreciated.

Test Class with the custom web application factory

 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);
        }
}

Custom Web Application Factory

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();

            });
        }
    }

My startup.cs in my application that I am testing

public IConfiguration Configuration { get; } public IHostingEnvironment Environment { get; }

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();

Update

I don't see this issue in my other example project where I am using only entity framework. I have a simple application with an application db context which uses SQL server. In my test class, I override it with an in-memory database and everything works. I am at a loss at to why it will work in my example application but not work in my main application. Is this something to do with how HangFire works?

In my test application (example code below), I can delete my sql database, run my test, and the test passes because the application DB context does not go looking for the sql server instance but uses the in-memory database. In my application, the HangFire service keeps trying to use the sql server database (if I delete the database and try to use an in-memory database for the test - it fails because it can't find the instance its trying to connect to). How come there is such a drastic difference in how the two projects work when a similar path is used for both?

I ran through the debugger for my integration test which calls the index method on the home controller above (using the CustomWebApplicationFactory). As I am initializing a test server, it goes through my startup class which calls below in ConfigureServices:

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

After that, the Configure method tries to call below statement:

app.UseHangfireServer();

At this point the test fails as It cannot find the DB. The DB is hosted on Azure so I am trying to replace it with an in-memory server for some of the integration test. Is the approach I am taking incorrect?


My example application where its working

Application DB Context in my example application

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 in my example application

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

CustomWebApplicationFactory - in my unit test project

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();

            });
        }
    }

My unit test in my unit test project

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


Update 2

I think I found an alternate to trying to override all my configuration in my integration test class. Since it's a lot more complicated to override HangFire as opposed to an ApplicationDBContext, I came up with below approach:

Startup.cs

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

Then in my CustomWebApplicationBuilder, I override the environment type for testing:

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();
           });
        }

    }

With that approach, I don't need to worry about having to do extra logic to satisfy hangfire's check for an active DB. It works but I am not 100% convinced its the best approach as I'm introducing branching in my production startup class.

like image 756
Help123 Avatar asked Nov 01 '25 13:11

Help123


1 Answers

There are two different scenarios you need to check.

  1. Create a job by class BackgroundJob
  2. Create a job by interface IBackgroundJobClient

For the first option, you could not replace the SqlServerStorage with MemoryStorage.

For UseSqlServerStorage, it will reset JobStorage by 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);
    }

Which means, no matter what you set in CustomWebApplicationFactory, UseSqlServerStorage will reset BackgroundJob with SqlServerStorage.

For second option, it could replace IBackgroundJobClient with MemoryStorage by

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

In conclusion, I suggest you register IBackgroundJobClient and try the second option to achieve your requirement.

Update1

For DB is not available, it could not be resolved by configuring the Dependency Injection. This error is caused by calling services.AddHangfire(x => x.UseSqlServerStorage(Configuration.GetConnectionString("ASP_NetPractice")));.

For resolving this error, you need to overriding this code in Startup.cs.

Try steps below:

  • Change Startup to below:

    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"))
        );
    }
    }
    
  • Create StartupTest in test project.

    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>();
    }
    }
    
  • Test

    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();
    }
    }
    
like image 142
Edward Avatar answered Nov 04 '25 02:11

Edward



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!