Monday, 27 July 2020

C# - Mock Data and Write Unit Tests with Entity Framework


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.




Real Example 



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 { [TestClass] public class NonQueryTests { [TestMethod] 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 { [TestClass] public class QueryTests { [TestMethod] 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 { [TestClass] public class AsyncQueryTests { [TestMethod] 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); } } }