I like Kzu's take on building unit-testable domain models with EF code first. I've been playing around with some of the same ideas myself, which center around simple context abstractions.
public interface IDomainContext { IQueryable<Restaurant> Restaurants { get; } IQueryable<Recipe> Recipes { get; } int SaveChanges(); void Save<T>(T entity); void Delete<T>(T entity); }
It's easy to mock an IDbContext, but I wanted a fake. My first brute force implementation had loads of messy problems in the Save and Delete methods. Here is an excerpt.
class FakeDbContext : IDomainContext { ... HashSet<Restaurant> _restaurants = new HashSet<Restaurant>( HashSet<Recipe> _recipes = new HashSet<Recipe>(); public void Save<T>(T entity) { var type = entity.GetType(); if (type == typeof(Restaurant)) _restaurants.Add(entity as Restaurant); if (type == typeof(Recipe)) _recipes.Add(entity as Recipe); }
... }
The problem is how each new entity type requires changes inside almost every method. In a classic case of overthinking, I thought I wanted something like EF's Set<T> API, where I can invoke Set<Recipe>() and get back HashSet<Recipe>. That's when I went off and built something to map Type T to HashSet<T>, based on .NET's KeyedCollection.
class HashSetMap : KeyedCollection<Type, object> { public HashSet<T> Get<T>() { return this[typeof(T)] as HashSet<T>; } protected override Type GetKeyForItem(object item) { return item.GetType().GetGenericArguments().First(); } }
The HashSetMap seemed to help the implementation of the FakeDbContext - at least I deleted all the if statements.
private HashSetMap _map; public FakeDbContext() { _map = new HashSetMap(); _map.Add(new HashSet<Restaurant>()); _map.Add(new HashSet<Recipe>()); } public void Save<T>(T entity) { _map.Get<T>().Add(entity); }
I was thinking this was clever, which was the first clue I did something wrong, and eventually I realized the idea of keeping a separate HashSet for each Type was ludicrous. Finally, the code became simpler.
class FakeDbContext : IDomainContext { HashSet<object> _entities = new HashSet<object>(); public IQueryable<Restaurant> Restaurants { get { return _entities.OfType<Restaurant>() .AsQueryable(); } } public IQueryable<Recipe> Recipes { get { return _entities.OfType<Recipe>() .AsQueryable(); } } public void SaveChanges() { ChangesSaved = true; } public bool ChangesSaved { get; set; } public void Save<T>(T entity) { _entities.Add(entity); } public void Delete<T>(T entity) { _entities.Remove(entity); } }
Usage:
[SetUp] public void Setup() { _db = new FakeDbContext(); for (int i = 0; i < 5; i++) { _db.Save(new Recipe()); } } [Test] public void Index_Action_Model_Is_Three_Recipes() { var controller = new RecipeController(_db); var result = controller.Index() as ViewResult; var model = result.Model as IEnumerable<Recipe>; Assert.AreEqual(3, model.Count()); }