Caution

This documentation is for EF7 onwards. For EF6.x and earlier release see http://msdn.com/data/ef.

Testing with InMemory

This article covers how to use the InMemory provider to write efficient tests with minimal impact to the code being tested.

Caution

Currently you need to use ServiceCollection and IServiceProvider to control the scope of the InMemory database, which adds complexity to your tests. We have a feature on our backlog to provide an easier mechanism for controlling the scope of InMemory databases.

In this article:

Tip

You can view this article’s sample on GitHub.

When to use InMemory for testing

The InMemory provider is useful when you want to test components using something that approximates connecting to the real database, without the overhead of actual database operations.

For example, consider the following service that allows application code to perform some operations related to blogs. Internally it uses a DbContext that connects to a SQL Server database. It would be useful to swap this context to connect to an InMemory database so that we can write efficient tests for this service without having to modify the code, or do a lot of work to create a test double of the context.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
    public class BlogService
    {
        private BloggingContext _context;

        public BlogService(BloggingContext context)
        {
            _context = context;
        }

        public void Add(string url)
        {
            var blog = new Blog { Url = url };
            _context.Blogs.Add(blog);
            _context.SaveChanges();
        }

        public IEnumerable<Blog> Find(string term)
        {
            return _context.Blogs
                .Where(b => b.Url.Contains(term))
                .OrderBy(b => b.Url)
                .ToList();
        }
    }

InMemory is not a relational database

EF Core database providers do not have to be relational databases. InMemory is designed to be a general purpose database for testing, and is not designed to mimic a relational database.

Some examples of this include:
  • InMemory will allow you to save data that would violate referential integrity constraints in a relational database.
  • If you use DefaultValueSql(string) for a property in your model, this is a relational database API and will have no effect when running against InMemory.

Tip

For many test purposes these difference will not matter. However, if you want to test against something that behaves more like a true relational database, then consider using SQLite in-memory mode.

Get your context ready

Avoid configuring two database providers

In your tests you are going to externally configure the context to use the InMemory provider. If you are configuring a database provider by overriding OnConfiguring in your context, then you need to add some conditional code to ensure that you only configure the database provider if one has not already been configured.

Note

If you are using ASP.NET Core, then you should not need this code since your database provider is configured outside of the context (in Startup.cs).

1
2
3
4
5
6
        protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        {
            if (!optionsBuilder.IsConfigured)
            {
                optionsBuilder.UseSqlServer(@"Server=(localdb)\mssqllocaldb;Database=EFProviders.InMemory;Trusted_Connection=True;");
            }

Add a constructor for testing

The simplest way to enable testing with the InMemory provider is to modify your context to expose a constructor that accepts an IServiceProvider and DbContextOptions<TContext>.

1
2
3
4
5
6
7
8
    public class BloggingContext : DbContext
    {
        public BloggingContext()
        { }

        public BloggingContext(IServiceProvider serviceProvider, DbContextOptions<BloggingContext> options)
            : base(serviceProvider, options)
        { }

Note

IServiceProvider is the container that EF will resolve all its services from (including the InMemory database instance). Typically, EF creates a single IServiceProvider for all contexts of a given type in an AppDomain. By allowing one to be passed in, you can control the scope of the InMemory database.

DbContextOptions<TContext> tells the context all of it’s settings, such as which database to connect to. This is the same object that is built by running the OnConfiguring method in your context.

Writing tests

The key to testing with this provider is the ability to tell the context to use the InMemory provider, and control the scope of the in-memory database. Typically you want a clean database for each test method.

Here is an example of a test class that uses the InMemory database. Each test method creates a new IServiceProvider, meaning each method has its own InMemory database.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
using BusinessLogic;
using Microsoft.Data.Entity;
using Microsoft.Data.Entity.Infrastructure;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System.Linq;

namespace TestProject
{
    [TestClass]
    public class BlogServiceTests
    {
        private ServiceCollection _serviceCollection;
        private DbContextOptions<BloggingContext> _contextOptions;

        public BlogServiceTests()
        {
            // Create a service collection that we can create service providers from
            // A service collection defines the services that will be available in service 
            // provider instances (think of it as ServiceProviderBuilder)
            _serviceCollection = new ServiceCollection();
            _serviceCollection.AddEntityFramework().AddInMemoryDatabase();

            // Create options to tell the context to use the InMemory database
            var optionsBuilder = new DbContextOptionsBuilder<BloggingContext>();
            optionsBuilder.UseInMemoryDatabase();
            _contextOptions = optionsBuilder.Options;
        }

        [TestMethod]
        public void Add_writes_to_database()
        {
            // All contexts that share the same service provider will share the same InMemory database
            var serviceProvider = _serviceCollection.BuildServiceProvider();

            // Run the test against one instance of the context
            using (var context = new BloggingContext(serviceProvider, _contextOptions))
            {
                var service = new BlogService(context);
                service.Add("http://sample.com");
            }

            // User a seperate instance of the context to verify correct data was saved to database
            using (var context = new BloggingContext(serviceProvider, _contextOptions))
            {
                Assert.AreEqual(1, context.Blogs.Count());
                Assert.AreEqual("http://sample.com", context.Blogs.Single().Url);
            }
        }

        [TestMethod]
        public void Find_searches_url()
        {
            // All contexts that share the same service provider will share the same InMemory database
            var serviceProvider = _serviceCollection.BuildServiceProvider();

            // Insert seed data into the database using one instance of the context
            using (var context = new BloggingContext(serviceProvider, _contextOptions))
            {
                context.Blogs.Add(new Blog { Url = "http://sample.com/cats" });
                context.Blogs.Add(new Blog { Url = "http://sample.com/catfish" });
                context.Blogs.Add(new Blog { Url = "http://sample.com/dogs" });
                context.SaveChanges();
            }

            // Use a clean instance of the context to run the test
            using (var context = new BloggingContext(serviceProvider, _contextOptions))
            {
                var service = new BlogService(context);
                var result = service.Find("cat");
                Assert.AreEqual(2, result.Count());
            }
        }
    }
}

Sharing a database instance for read-only tests

If a test class has read-only tests that share the same seed data, then you can share the InMemory database instance for the whole class (rather than a new one for each method). This means you have a single IServiceProvider for test class, rather than one for each test method.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Microsoft.Extensions.DependencyInjection;
using BusinessLogic;
using Microsoft.Data.Entity;
using System.Linq;
using System;
using Microsoft.Data.Entity.Infrastructure;

namespace TestProject
{
    [TestClass]
    public class BlogServiceTestsReadOnly
    {
        private IServiceProvider _serviceProvider;
        private DbContextOptions<BloggingContext> _contextOptions;

        public BlogServiceTestsReadOnly()
        {
            // Create a service provider to be shared by all test methods
            var serviceCollection = new ServiceCollection();
            serviceCollection.AddEntityFramework().AddInMemoryDatabase();
            _serviceProvider = serviceCollection.BuildServiceProvider();

            // Create options to tell the context to use the InMemory database
            var optionsBuilder = new DbContextOptionsBuilder<BloggingContext>();
            optionsBuilder.UseInMemoryDatabase();
            _contextOptions = optionsBuilder.Options;

            // Insert the seed data that is expected by all test methods
            using (var context = new BloggingContext(_serviceProvider, _contextOptions))
            {
                context.Blogs.Add(new Blog { Url = "http://sample.com/cats" });
                context.Blogs.Add(new Blog { Url = "http://sample.com/catfish" });
                context.Blogs.Add(new Blog { Url = "http://sample.com/dogs" });
                context.SaveChanges();
            }
        }

        [TestMethod]
        public void Find_with_empty_term()
        {
            using (var context = new BloggingContext(_serviceProvider, _contextOptions))
            {
                var service = new BlogService(context);
                var result = service.Find("");
                Assert.AreEqual(3, result.Count());
            }
        }

        [TestMethod]
        public void Find_with_unmatched_term()
        {
            using (var context = new BloggingContext(_serviceProvider, _contextOptions))
            {
                var service = new BlogService(context);
                var result = service.Find("horse");
                Assert.AreEqual(0, result.Count());
            }
        }

        [TestMethod]
        public void Find_with_some_matched()
        {
            using (var context = new BloggingContext(_serviceProvider, _contextOptions))
            {
                var service = new BlogService(context);
                var result = service.Find("cat");
                Assert.AreEqual(2, result.Count());
            }
        }
    }
}

Advanced: How to avoid modifying the context

It is much more complicated, but you can avoid adding a constructor to your context. This approach leverages more advanced functionality of IServiceProvider.

Here is an example that uses this approach. The important points are:
  • Create a global ServiceCollection, register the context as a service, and configure it to use the InMemory database.
  • Create an IServiceProvider for each InMemory database instance you want (in this case, one per test method).
  • Create an IServiceScope for each context instance you want, and resolve the context from it.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Microsoft.Extensions.DependencyInjection;
using BusinessLogic;
using Microsoft.Data.Entity;
using System.Linq;

namespace TestProject
{
    [TestClass]
    public class BlogServiceTestsAdvanced
    {
        private ServiceCollection _serviceCollection;

        public BlogServiceTestsAdvanced()
        {
            // Create a service collection to be shared by all test methods
            _serviceCollection = new ServiceCollection();
            _serviceCollection
                .AddEntityFramework()
                .AddInMemoryDatabase()
                .AddDbContext<BloggingContext>(c => c.UseInMemoryDatabase());
        }

        [TestMethod]
        public void Add_writes_to_database()
        {
            // All contexts created from this service provider will access the same InMemory database
            var serviceProvider = _serviceCollection.BuildServiceProvider();

            // Run the test against one instance of the context
            using (var scope = serviceProvider.GetRequiredService<IServiceScopeFactory>().CreateScope())
            {
                var context = scope.ServiceProvider.GetService<BloggingContext>();
                var service = new BlogService(context);
                service.Add("http://sample.com");
            }

            // User a seperate instance of the context to verify correct data was saved to database
            using (var scope = serviceProvider.GetRequiredService<IServiceScopeFactory>().CreateScope())
            {
                var context = scope.ServiceProvider.GetService<BloggingContext>();
                Assert.AreEqual(1, context.Blogs.Count());
                Assert.AreEqual("http://sample.com", context.Blogs.Single().Url);
            }
        }

        [TestMethod]
        public void Find_searches_url()
        {
            // All contexts created from this service provider will access the same InMemory database
            var serviceProvider = _serviceCollection.BuildServiceProvider();

            // Insert seed data into the database using one instance of the context
            using (var scope = serviceProvider.GetRequiredService<IServiceScopeFactory>().CreateScope())
            {
                var context = scope.ServiceProvider.GetService<BloggingContext>();
                context.Blogs.Add(new Blog { Url = "http://sample.com/cats" });
                context.Blogs.Add(new Blog { Url = "http://sample.com/catfish" });
                context.Blogs.Add(new Blog { Url = "http://sample.com/dogs" });
                context.SaveChanges();
            }

            // Use a clean instance of the context to run the test
            using (var scope = serviceProvider.GetRequiredService<IServiceScopeFactory>().CreateScope())
            {
                var context = scope.ServiceProvider.GetService<BloggingContext>();
                var service = new BlogService(context);
                var result = service.Find("cat");
                Assert.AreEqual(2, result.Count());
            }
        }
    }
}