How to mock data and write unit tests
with Entity Framework?
Entity Framework isn't
unit test friendly, so how to mock Entity Framework's DbContext for Unit
Testing?
Entity Framework has
an InMemory provider that is useful when you want to test
functionality using something that approximates connecting to a real database
in your Unit Tests.
Entity Framework Effort
Effort is a powerful tool that enables a
convenient way to create automated tests for Entity Framework based
applications.
A simple
modification is enough to make Entity Framework use a fake in-memory database.
public List<Customer> GetAllCustomers()
{
using(MyContext context = Effort.ObjectContextFactory.CreateTransient<MyContext>())
{
return context.Customers.ToList();
}
}
Moq
Moq is
the only mocking library for .NET developed from scratch to take full advantage
of .NET Linq expression trees and lambda expressions, which makes it the most
productive, type-safe and refactoring-friendly mocking library available.
public List<Customer> GetAllCustomers()
{
var mockSet = new Mock<DbSet<Customer>>();
mockSet.As<IQueryable<Customer>>().Setup(m => m.Provider).Returns(data.Provider);
mockSet.As<IQueryable<Customer>>().Setup(m => m.Expression).Returns(data.Expression);
mockSet.As<IQueryable<Customer>>().Setup(m => m.ElementType).Returns(data.ElementType);
mockSet.As<IQueryable<Customer>>().Setup(m => m.GetEnumerator()).Returns(data.GetEnumerator());
var mockContext = new Mock<MyContext>();
mockContext.Setup(c => c.Customers).Returns(mockSet.Object);
var service = new CustomerService(mockContext.Object);
return service.GetAllCustomers();
}
NSubstitute
NSubstitute
is designed as a friendly substitute for .NET mocking libraries.
public List<Customer> GetAllCustomers()
{
IDbSet<Customer> customerDbSet = Substitute.For<IDbSet<Customer>>();
customerDbSet.Provider.Returns(customers.Provider);
customerDbSet.Expression.Returns(customers.Expression);
customerDbSet.ElementType.Returns(customers.ElementType);
customerDbSet.GetEnumerator().Returns(customers.GetEnumerator());
IRepositoryContext repositoryContext = Substitute.For<IRepositoryContext>();
repositoryContext.Customers.Returns(customerDbSet);
return repositoryContext.Customers.ToList();
}
This
provides us with an IDbSet fake
that we can return from an IRepositoryContext fake.
The EF Model
using System.Collections.Generic; using System.Data.Entity; namespace TestingDemo { public class BloggingContext : DbContext { public virtual DbSet<Blog> Blogs { get; set; } public virtual DbSet<Post> Posts { get; set; } } public class Blog { public int BlogId { get; set; } public string Name { get; set; } public string Url { get; set; } public virtual List<Post> Posts { get; set; } } public class Post { public int PostId { get; set; } public string Title { get; set; } public string Content { get; set; } public int BlogId { get; set; } public virtual Blog Blog { get; set; } } }
Virtual DbSet Properties with EF Designer
If you are using Code First then you can edit your classes directly. If you are using the EF Designer then you’ll need to edit the T4 template that generates your context. Open up the <model_name>.Context.tt file that is nested under you edmx file, find the following fragment of code and add in the virtual keyword as shown.
public string DbSet(EntitySet entitySet) { return string.Format( CultureInfo.InvariantCulture, "{0} virtual DbSet\<{1}> {2} {{ get; set; }}", Accessibility.ForReadOnlyProperty(entitySet), _typeMapper.GetTypeName(entitySet.ElementType), _code.Escape(entitySet)); }
Service To Be Tested
using System.Collections.Generic; using System.Data.Entity; using System.Linq; using System.Threading.Tasks; namespace TestingDemo { public class BlogService { private BloggingContext _context; public BlogService(BloggingContext context) { _context = context; } public Blog AddBlog(string name, string url) { var blog = _context.Blogs.Add(new Blog { Name = name, Url = url }); _context.SaveChanges(); return blog; } public List<Blog> GetAllBlogs() { var query = from b in _context.Blogs orderby b.Name select b; return query.ToList(); } public async Task<List<Blog>> GetAllBlogsAsync() { var query = from b in _context.Blogs orderby b.Name select b; return await query.ToListAsync(); } } }
Testing Non-Query Scenarios
using Microsoft.VisualStudio.TestTools.UnitTesting; using Moq; using System.Data.Entity; namespace TestingDemo { [] public class NonQueryTests { [] public void CreateBlog_saves_a_blog_via_context() { var mockSet = new Mock<DbSet<Blog>>(); var mockContext = new Mock<BloggingContext>(); mockContext.Setup(m => m.Blogs).Returns(mockSet.Object); var service = new BlogService(mockContext.Object); service.AddBlog("ADO.NET Blog", "http://blogs.msdn.com/adonet"); mockSet.Verify(m => m.Add(It.IsAny<Blog>()), Times.Once()); mockContext.Verify(m => m.SaveChanges(), Times.Once()); } } }
Testing Query Scenarios
using Microsoft.VisualStudio.TestTools.UnitTesting; using Moq; using System.Collections.Generic; using System.Data.Entity; using System.Linq; namespace TestingDemo { [] public class QueryTests { [] public void GetAllBlogs_orders_by_name() { var data = new List<Blog> { new Blog { Name = "BBB" }, new Blog { Name = "ZZZ" }, new Blog { Name = "AAA" }, }.AsQueryable(); var mockSet = new Mock<DbSet<Blog>>(); mockSet.As<IQueryable<Blog>>().Setup(m => m.Provider).Returns(data.Provider); mockSet.As<IQueryable<Blog>>().Setup(m => m.Expression).Returns(data.Expression); mockSet.As<IQueryable<Blog>>().Setup(m => m.ElementType).Returns(data.ElementType); mockSet.As<IQueryable<Blog>>().Setup(m => m.GetEnumerator()).Returns(data.GetEnumerator()); var mockContext = new Mock<BloggingContext>(); mockContext.Setup(c => c.Blogs).Returns(mockSet.Object); var service = new BlogService(mockContext.Object); var blogs = service.GetAllBlogs(); Assert.AreEqual(3, blogs.Count); Assert.AreEqual("AAA", blogs[0].Name); Assert.AreEqual("BBB", blogs[1].Name); Assert.AreEqual("ZZZ", blogs[2].Name); } } }
Testing with Async Queries
using System.Collections.Generic; using System.Data.Entity.Infrastructure; using System.Linq; using System.Linq.Expressions; using System.Threading; using System.Threading.Tasks; namespace TestingDemo { internal class TestDbAsyncQueryProvider<TEntity> : IDbAsyncQueryProvider { private readonly IQueryProvider _inner; internal TestDbAsyncQueryProvider(IQueryProvider inner) { _inner = inner; } public IQueryable CreateQuery(Expression expression) { return new TestDbAsyncEnumerable<TEntity>(expression); } public IQueryable<TElement> CreateQuery<TElement>(Expression expression) { return new TestDbAsyncEnumerable<TElement>(expression); } public object Execute(Expression expression) { return _inner.Execute(expression); } public TResult Execute<TResult>(Expression expression) { return _inner.Execute<TResult>(expression); } public Task<object> ExecuteAsync(Expression expression, CancellationToken cancellationToken) { return Task.FromResult(Execute(expression)); } public Task<TResult> ExecuteAsync<TResult>(Expression expression, CancellationToken cancellationToken) { return Task.FromResult(Execute<TResult>(expression)); } } internal class TestDbAsyncEnumerable<T> : EnumerableQuery<T>, IDbAsyncEnumerable<T>, IQueryable<T> { public TestDbAsyncEnumerable(IEnumerable<T> enumerable) : base(enumerable) { } public TestDbAsyncEnumerable(Expression expression) : base(expression) { } public IDbAsyncEnumerator<T> GetAsyncEnumerator() { return new TestDbAsyncEnumerator<T>(this.AsEnumerable().GetEnumerator()); } IDbAsyncEnumerator IDbAsyncEnumerable.GetAsyncEnumerator() { return GetAsyncEnumerator(); } IQueryProvider IQueryable.Provider { get { return new TestDbAsyncQueryProvider<T>(this); } } } internal class TestDbAsyncEnumerator<T> : IDbAsyncEnumerator<T> { private readonly IEnumerator<T> _inner; public TestDbAsyncEnumerator(IEnumerator<T> inner) { _inner = inner; } public void Dispose() { _inner.Dispose(); } public Task<bool> MoveNextAsync(CancellationToken cancellationToken) { return Task.FromResult(_inner.MoveNext()); } public T Current { get { return _inner.Current; } } object IDbAsyncEnumerator.Current { get { return Current; } } } }
using Microsoft.VisualStudio.TestTools.UnitTesting; using Moq; using System.Collections.Generic; using System.Data.Entity; using System.Data.Entity.Infrastructure; using System.Linq; using System.Threading.Tasks; namespace TestingDemo { [] public class AsyncQueryTests { [] public async Task GetAllBlogsAsync_orders_by_name() { var data = new List<Blog> { new Blog { Name = "BBB" }, new Blog { Name = "ZZZ" }, new Blog { Name = "AAA" }, }.AsQueryable(); var mockSet = new Mock<DbSet<Blog>>(); mockSet.As<IDbAsyncEnumerable<Blog>>() .Setup(m => m.GetAsyncEnumerator()) .Returns(new TestDbAsyncEnumerator<Blog>(data.GetEnumerator())); mockSet.As<IQueryable<Blog>>() .Setup(m => m.Provider) .Returns(new TestDbAsyncQueryProvider<Blog>(data.Provider)); mockSet.As<IQueryable<Blog>>().Setup(m => m.Expression).Returns(data.Expression); mockSet.As<IQueryable<Blog>>().Setup(m => m.ElementType).Returns(data.ElementType); mockSet.As<IQueryable<Blog>>().Setup(m => m.GetEnumerator()).Returns(data.GetEnumerator()); var mockContext = new Mock<BloggingContext>(); mockContext.Setup(c => c.Blogs).Returns(mockSet.Object); var service = new BlogService(mockContext.Object); var blogs = await service.GetAllBlogsAsync(); Assert.AreEqual(3, blogs.Count); Assert.AreEqual("AAA", blogs[0].Name); Assert.AreEqual("BBB", blogs[1].Name); Assert.AreEqual("ZZZ", blogs[2].Name); } } }