EF CTP4 Tips & Tricks: Testing With Fake DbContext

Posted on September 7, 2010. Filed under: Entity Framework | Tags: , , , , , , , , |

 


This post is based on a pre-release version of Entity Framework.

An updated post is available here.


 

 

This is the sixth in a series of posts about the recently released Entity Framework Feature CTP4, now affectionately known as “EF Magic Unicorn Edition”.

In this post we are going to take a look at how to replace a DbContext based data access layer with an in-memory fake for unit testing.

I’m going to use an MVC controller as an example but the same approach can be used for any component that consumes a derived DbContext, including repositories.

The Problem

Say we have a very simple model for Employees and Departments and we are using DbContext in Code First mode to persist and query our data:

public class EmployeeContext : DbContext
{
public DbSet<Department> Departments { get; set; }
public DbSet<Employee> Employees { get; set; }
}

public class Department
{
public int DepartmentId { get; set; }
public string Name { get; set; }

public ICollection<Employee> Employees { get; set; }
}

public class Employee
{
public int EmployeeId { get; set; }
public int FirstName { get; set; }
public int LastName { get; set; }
public int Position { get; set; }

public Department Department { get; set; }
}

In an MVC application we could add a controller that can display all departments in alphabetical order and allow us to add new departments:

(Typically we’d also implement ‘detail’, ‘edit’ and ‘delete’ functionality but I’m leaving them out because ‘list’ and ‘create’ are enough to demonstrate fakes)

public class DepartmentController : Controller
{
private EmployeeContext context;

public DepartmentController()
{
this.context = new EmployeeContext();
}

public ViewResult Index()
{
return View(this.context.Departments.OrderBy(d => d.Name).ToList());
}

public ViewResult Create()
{
return View();
}

[HttpPost]
public ActionResult Create(Department dep)
{
context.Departments.Add(dep);
context.SaveChanges();
return RedirectToAction("Index");
}
}

The issue is that our controller now has a hard dependency on EF because it needs an EmployeeContext which derives from DbContext and exposes DbSets. We have no way to replace this with any other implementation and we are forced to run any unit tests against a real database, which means they really aren’t unit tests at all.

Adding an Interface

DbSet<T> happens to implement IDbSet<T> so we can pretty easily create an interface that our derived context implements:

public interface IEmployeeContext
{
IDbSet&lt;Department&gt; Departments { get; }
IDbSet&lt;Employee&gt; Employees { get; }
int SaveChanges();
}

public class EmployeeContext : DbContext, IEmployeeContext
{
public IDbSet&lt;Department&gt; Departments { get; set; }
public IDbSet&lt;Employee&gt; Employees { get; set; }
}

Now we can update our controller to be based on this interface rather than the EF specific implementation. You’ll notice I’m still using the EF implementation in the default constructor, in a real world app you would probably just have the interface based constructor and use dependency injection to supply the EF implementation at runtime, but I just wanted to keep things simple for this post🙂

public class DepartmentController : Controller
{
private IEmployeeContext context;

public DepartmentController()
{
this.context = new EmployeeContext();
}

public DepartmentController(IEmployeeContext context)
{
this.context = context;
}

public ViewResult Index()
{
return View(this.context.Departments.OrderBy(d =&gt; d.Name).ToList());
}

public ViewResult Create()
{
return View();
}

[HttpPost]
public ActionResult Create(Department dep)
{
context.Departments.Add(dep);
context.SaveChanges();
return RedirectToAction(&quot;Index&quot;);
}
}

Building Fakes

The first thing we need is a fake implementation of IDbSet<TEntity>, this is pretty easy to implement.

public class FakeDbSet&lt;T&gt; : IDbSet&lt;T&gt;
where T : class
{
HashSet&lt;T&gt; _data;
IQueryable _query;

public FakeDbSet()
{
_data = new HashSet&lt;T&gt;();
_query = _data.AsQueryable();
}

public virtual T Find(params object[] keyValues)
{
throw new NotImplementedException(&quot;Derive from FakeDbSet&lt;T&gt; and override Find&quot;);
}

public void Add(T item)
{
_data.Add(item);
}

public void Remove(T item)
{
_data.Remove(item);
}

public void Attach(T item)
{
_data.Add(item);
}

public void Detach(T item)
{
_data.Remove(item);
}

Type IQueryable.ElementType
{
get { return _query.ElementType; }
}

System.Linq.Expressions.Expression IQueryable.Expression
{
get { return _query.Expression; }
}

IQueryProvider IQueryable.Provider
{
get { return _query.Provider; }
}

System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
{
return _data.GetEnumerator();
}

IEnumerator&lt;T&gt; IEnumerable&lt;T&gt;.GetEnumerator()
{
return _data.GetEnumerator();
}
}

The one thing you’ll notice is that there isn’t really a good way to generically implement Find so I’ve left it as a virtual method that throws if called. If our application makes use of the Find method we can create an implementation specific to each type:

public class FakeDepartmentSet : FakeDbSet&lt;Department&gt;
{
public override Department Find(params object[] keyValues)
{
return this.SingleOrDefault(d =&gt; d.DepartmentId == (int)keyValues.Single());
}
}

public class FakeEmployeeSet : FakeDbSet&lt;Employee&gt;
{
public override Employee Find(params object[] keyValues)
{
return this.SingleOrDefault(e =&gt; e.EmployeeId == (int)keyValues.Single());
}
}

Now we can create a fake implementation of our context:

public class FakeEmployeeContext : IEmployeeContext
{
public FakeEmployeeContext()
{
this.Departments = new FakeDepartmentSet();
this.Employees = new FakeEmployeeSet();
}

public IDbSet&lt;Department&gt; Departments { get; private set; }

public IDbSet&lt;Employee&gt; Employees { get; private set; }

public int SaveChanges()
{ return 0; }
}

Testing Against Fakes

Now that we have our fakes defined we can use them to write a unit test for our controller, that doesn’t use EF:

[TestMethod]
public void IndexOrdersByName()
{
var context = new FakeEmployeeContext
{
Departments =
{
new Department { Name = &quot;BBB&quot;},
new Department { Name = &quot;AAA&quot;},
new Department { Name = &quot;ZZZ&quot;},
}
};

var controller = new DepartmentController(context);
var result = controller.Index();

Assert.IsInstanceOfType(result.ViewData.Model, typeof(IEnumerable&lt;Department&gt;));
var departments = (IEnumerable&lt;Department&gt;)result.ViewData.Model;
Assert.AreEqual(&quot;AAA&quot;, departments.ElementAt(0).Name);
Assert.AreEqual(&quot;BBB&quot;, departments.ElementAt(1).Name);
Assert.AreEqual(&quot;ZZZ&quot;, departments.ElementAt(2).Name);
}

Summary

In this post we saw how to build an interface that represents our context and how to build an in-memory fake of that context for use in our unit tests. We used an MVC controller to demonstrate this but the same approach can be used with any component that needs to interact with an EF based context, including repositories. There are a number of reasons to use in-memory fakes for unit testing but some key benefits are stable and robust tests that execute quickly and exercise a single component, making failures easy to isolate.

    About

    Rowan works as a Program Manager for the ADO.NET Entity Framework team at Microsoft. He speaks at technical conferences and blogs at romiller.com. Rowan lives in Seattle, Washington with his wife Athalie. Prior to moving to the US he resided in the small state of Tasmania in Australia. Outside of technology Rowan's passions include snowboarding, mountain biking, horse riding, rock climbing and pretty much anything else that involves being active. The primary focus of his life, however, is to follow Jesus.

    RSS

    Subscribe Via RSS

    • Subscribe with Bloglines
    • Add your feed to Newsburst from CNET News.com
    • Subscribe in Google Reader
    • Add to My Yahoo!
    • Subscribe in NewsGator Online
    • The latest comments to all posts in RSS

    Meta

Liked it here?
Why not try sites on the blogroll...

%d bloggers like this: