7

I've been following the strategies for setting up tests for an ASP.NET Core 2.2 API using the Microsoft documentation at Integration tests in ASP.NET Core.

To summarize, we extend and customize WebApplicationFactory and use a IWebHostBuilder to setup and configure various services to provide us with a database context using an in-memory database for testing like below (copied and pasted from the article):

public class CustomWebApplicationFactory<TStartup> 
    : WebApplicationFactory<TStartup> where TStartup: class
{
    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();

            // Create a scope to obtain a reference to the database
            // context (ApplicationDbContext).
            using (var scope = sp.CreateScope())
            {
                var scopedServices = scope.ServiceProvider;
                var db = scopedServices.GetRequiredService<ApplicationDbContext>();
                var logger = scopedServices
                    .GetRequiredService<ILogger<CustomWebApplicationFactory<TStartup>>>();

                // Ensure the database is created.
                db.Database.EnsureCreated();

                try
                {
                    // Seed the database with test data.
                    Utilities.InitializeDbForTests(db);
                }
                catch (Exception ex)
                {
                    logger.LogError(ex, $"An error occurred seeding the " +
                        "database with test messages. Error: {ex.Message}");
                }
            }
        });
    }
}

In the tests we can use the factory and create a client like so:

public class IndexPageTests : 
    IClassFixture<CustomWebApplicationFactory<RazorPagesProject.Startup>>
{
    private readonly HttpClient _client;
    private readonly CustomWebApplicationFactory<RazorPagesProject.Startup> 
        _factory;

    public IndexPageTests(
        CustomWebApplicationFactory<RazorPagesProject.Startup> factory)
    {
        _factory = factory;
        _client = factory.CreateClient(new WebApplicationFactoryClientOptions
            {
                AllowAutoRedirect = false
            });
    }

    [Fact]
    public async Task Test1()
    {
        var response = await _client.GetAsync("/api/someendpoint");
    }
}

This works fine, but note the call to InitializeDbForTests which sets up some test data for all tests when the services are configured.

I'd like a reasonable strategy for starting every API test with a clean slate, so that tests don't become dependent on each other. I've been looking for various ways to get a hold of ApplicationDbContext in my test methods to no avail.

Would it be reasonable to do integration tests in complete isolation from each other, and how could I approach it using ASP.NET Core / EF Core / xUnit.NET?

3 Answers 3

10

Ironically, you're looking for EnsureDeleted instead of EnsureCreated. That will dump the database. Since the in-memory "database" is schemaless, you don't actually need to ensure it is created or even migrate it.

Additionally, you should not be using a hard-coded name for the in-memory database. That will actually cause the same database instance in-memory to be used everywhere. Instead, you should use something random: Guid.NewGuid().ToString() is good enough.

Sign up to request clarification or add additional context in comments.

9 Comments

Then my question is where would I call that method? As far as I understand it, InitializeDbForTests runs once and once only, and I cannot directly access the context object from my tests. Maybe I'm misunderstanding something? As per your second comment, would the DB re-init from scratch if it would be given a ranom name?
You call it in the same place you're calling EnsureCreated currently: essentially, before you want to fill it with data.
Right, thanks. What I'm trying to ask is how I can run InitializeDbForTests (or equivalent functionality) once for every test instead of only once before running all tests. As the example from the link above stands, all methods within one fixture share the same seeded data and could therefore affect each other.
Gotcha. Yeah, just pull it out of the factory setup and drop it in your test setup (test class constructor). The factory has a Host member which itself has a Services member - an instance of IServiceProvider, i.e. _factory.Host.Services.GetRequiredService<Foo>().
Just realized I went too simplistic on that example. The context, of course is scoped, so you'd actually need to do _factory.Host.Services.CreateScope().
|
3

Alright, so I got it working! Getting the scoped service was the key. When I want to seed from scratch, I can start each test with wrapping the seeding call in a

using (var scope = _factory.Server.Host.Services.CreateScope()) { }

section where I can first

var scopedServices = scope.ServiceProvider;

and then

var db = scopedServices.GetRequiredService<MyDbContext>();

before

db.Database.EnsureDeleted()

and finally running my seeding functions. A bit clunky but it works.

Thanks to Chris Pratt for the help (Answer from comment).

Comments

0

Actually, Testing with InMemory describes the process really well in the section titled "Writing Tests". Here's some code that illustrates the basic idea

    [TestClass]
public class BlogServiceTests
{
    [TestMethod]
    public void Add_writes_to_database()
    {
        var options = new DbContextOptionsBuilder<BloggingContext>()
            .UseInMemoryDatabase(databaseName: "Add_writes_to_database")
            .Options;

The idea is there is a separate database per test method so you don't have to worry about the order that tests are being run or the fact that they are running in parallel. Of course, you'll have to add some code that populates you database and call it from every test method.

I've used this technique and it works pretty well.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.