OdeToCode IC Logo

Faking DbContext

Wednesday, June 1, 2011

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());
}