dotnet / EntityFramework.Docs

Documentation for Entity Framework Core and Entity Framework 6
https://docs.microsoft.com/ef/
Creative Commons Attribution 4.0 International
1.59k stars 1.95k forks source link

Testing with InMemory #95

Closed rowanmiller closed 8 years ago

rowanmiller commented 8 years ago

The most important thing is how to scope the database to a single test...

using Microsoft.Data.Entity;
using Microsoft.Data.Entity.Infrastructure;
using Microsoft.Extensions.DependencyInjection;
using System;
using System.Linq;

namespace ConsoleApp1
{
    public class Program
    {
        public static void Main(string[] args)
        {
            var serviceCollection = new ServiceCollection();
            serviceCollection
                .AddEntityFramework()
                .AddInMemoryDatabase()
                .AddDbContext<SampleContext>(c => c.UseInMemoryDatabase());

            using (var db = serviceCollection.BuildServiceProvider().GetService<SampleContext>())
            {
                db.Blogs.Add(new Blog { Url = "Test" });
                db.SaveChanges();
                Console.WriteLine(db.Blogs.Count());
            }

            using (var db = serviceCollection.BuildServiceProvider().GetService<SampleContext>())
            {
                db.Blogs.Add(new Blog { Url = "Test" });
                db.SaveChanges();
                Console.WriteLine(db.Blogs.Count());
            }
        }
    }

    public class SampleContext : DbContext
    {
        public DbSet<Blog> Blogs { get; set; }
    }

    public class Blog
    {
        public int BlogId { get; set; }
        public string Url { get; set; }
    }
}
rowanmiller commented 8 years ago

Should also cover this point that was mentioned in #105

Keep DbContext's isolated; don't re-use the same context to seed the database and then query.

VeronicaWasson commented 8 years ago

I'm trying to write a test that uses two DbContext's, sharing the same in-memory DB. The idea is to create the entities using the first context, and pass a clean context to the code that I'm testing. That way I can validate that I'm loading navigation properties correctly, etc, in my EF7 code. The best I could find was to register my DbContext as a transient:

public class TestClass
{
    private readonly ServiceCollection _serviceCollection;
    private readonly DbContextOptions<MyDbContext> _options;

    public TestClass()
    {
        var optionsBuilder = new DbContextOptionsBuilder<MyDbContext>();
        optionsBuilder.UseInMemoryDatabase();
        _options = optionsBuilder.Options;

        var _serviceCollection = new ServiceCollection();
        _serviceCollection
            .AddEntityFramework()
            .AddInMemoryDatabase();

        _serviceCollection.AddTransient<MyDbContext>(provider => new MyDbContext(provider, _options));
    }

    [Fact]
    public Test1()
    {
        IServiceProvider provider = _serviceCollection.BuildServiceProvider();
        using (var context = provider.GetService<MyDbContext>())
        {
            context.Add(new Thing { Id = 1 });
            context.SaveChanges();
        }

        using (var context = provider.GetService<ApplicationDbContext>())
        {
            // test with a new context but same DB
        }
    }
}

It seems to work, but is there a better way to achieve this?

rowanmiller commented 8 years ago

@MikeWasson it's a bit of a "level up" on DI skills... but you can create scopes within a service provider to get new instances of scoped services... but the non-scoped services (such as the InMemory store) are shared between all scopes on the provider.

This actually emulates nicely what happens in ASP.NET - where a new scope is created for each request. So Test1() now approximates some data being inserted on one request and then some other logic happening on a second request that accesses the same database.

public class TestClass
{
    private readonly ServiceCollection _serviceCollection;

    public TestClass()
    {
        _serviceCollection = new ServiceCollection();
        _serviceCollection
            .AddEntityFramework()
            .AddInMemoryDatabase()
            .AddDbContext<SampleContext>(c => c.UseInMemoryDatabase());
    }

    [Fact]
    public Test1()
    {
        IServiceProvider provider = _serviceCollection.BuildServiceProvider();
        using (var scope = provider.GetRequiredService<IServiceScopeFactory>().CreateScope())
        {
            var context = scope.ServiceProvider.GetService<MyDbContext>())
            context.Add(new Thing { Id = 1 });
            context.SaveChanges();
        }

        using (var scope = provider.GetRequiredService<IServiceScopeFactory>().CreateScope())
        {
            var context = scope.ServiceProvider.GetService<MyDbContext>())
            // test with a new context but same DB
        }
    }
}
rowanmiller commented 8 years ago

Page is now available at http://docs.efproject.net/en/latest/miscellaneous/testing.html

@MikeWasson I came up with some patterns to make things a little simpler (though we still have plenty of room for improvement - mostly implementing https://github.com/aspnet/EntityFramework/issues/3253) so you should take a look at the final article.

Abrissirba commented 8 years ago

Awesome Rowan! :) That was exactly what I needed to understand how to scope my contexts :)

chenlonghu commented 8 years ago

@rowanmiller Thanks Rowan. Your doc was helpful !

funzeye commented 8 years ago

inheriting from IdentityDbContext throws a bit of spanner in the works with this: (http://docs.efproject.net/en/latest/miscellaneous/testing.html) - it doesnt contain a constructor that takes two arguments

rowanmiller commented 8 years ago

@funzeye this is all changing in the upcoming RC2 and IdentityDbContext will have a constructor that allows you to pass in all this stuff.

funzeye commented 8 years ago

cool, great work guys. EF FTW.

mgallig commented 7 years ago

@rowanmiller Hi. I reported this a while ago and since then the named dbcontext can be passed in but -- I find that the context is still shared.

I probably use a weird edge-case, but I am performing a bunch of integration tests in xunit (which work better than I could have ever hoped, with the exception of this problem) that spins up a TestServer with UseInMemoryDatabase for each test class.

The problem is, even with separate test servers, with separate dbcontexts, the underlying in memory database was still shared (each was created with a new Guid for a name)

I also get a "A second operation on this context....." type error that I'll never see in actual production all the time due to apparent context sharing.

ajcvickers commented 7 years ago

@mgallig Please file a new issue on https://github.com/aspnet/EntityFramework including a full code listing or project that will let us reproduce what you are seeing.

babu12f commented 5 years ago

where is ServiceCollection class

ajcvickers commented 5 years ago

@babu12f https://docs.microsoft.com/en-us/dotnet/api/microsoft.extensions.dependencyinjection.servicecollection?view=aspnetcore-2.1

logamanig commented 5 years ago

Following code works perfectly for me, // Requires separate DI Service provider to isolate db instance as InMemory database is also shared within // the class scope since the App Service Factory is configured to per scope instance for dbcontext var internalServiceProviderForDbContext = new ServiceCollection() .AddEntityFrameworkInMemoryDatabase() .BuildServiceProvider(); var options = new DbContextOptionsBuilder() .UseInternalServiceProvider(internalServiceProviderForDbContext) .UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString()) .Options;

ajcvickers commented 5 years ago

@logamanig It's generally best to avoid calling UseInternalServiceProvider if you don't really need to.

logamanig commented 5 years ago

@logamanig It's generally best to avoid calling UseInternalServiceProvider if you don't really need to.

without calling UseInternalServiceProvider, not able to isolate the InMemoryDatabase instance between test cases. Just a unique database name is not enough to isolate.

ajcvickers commented 5 years ago

See https://github.com/aspnet/EntityFrameworkCore/issues/3253#issuecomment-497724245

logamanig commented 5 years ago

Following code works perfectly for me, // Requires separate DI Service provider to isolate db instance as InMemory database is also shared within // the class scope since the App Service Factory is configured to per scope instance for dbcontext var internalServiceProviderForDbContext = new ServiceCollection() .AddEntityFrameworkInMemoryDatabase() .BuildServiceProvider(); var options = new DbContextOptionsBuilder() .UseInternalServiceProvider(internalServiceProviderForDbContext) .UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString()) .Options;

As pointed out by @ajcvickers , internal service provider is not required. specifying the inMemoryRoot in optionbuilder is the best way to do it.