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

Comments
gravatar Darren Cauthon Wednesday, June 1, 2011
What is that unit test doing?
gravatar Scott Allen Thursday, June 2, 2011
It's just a sample showing you can insert data into the fake during a test (and that a component under test can query the data out of the fake). I wouldn't read anything else into the test. Sorry if it is confusing or misleading.
Comments are closed.

My Pluralsight Courses

K.Scott Allen OdeToCode by K. Scott Allen
What JavaScript Developers Should Know About ECMAScript 2015
The Podcast!