Wednesday, April 14, 2010

WCF RIA, Repository pattern and testability (Part 2)

Ok, let’s continue…

The idea of testability based on the ability to substitute execution context. And we usually achieve that by using IoC containers. However, I am strong follower of concept “Simplest thing that works” that’s why I introduce you to DataContext

Code Snippet
/// <summary>
/// Keeps ObjectContext for current request
/// </summary>
public static class DataContext
{
    [ThreadStatic] private static IObjectContext _current;

    public static IObjectContext Current
    {
        get { return _current; }
        internal set { _current = value; } // must be set only from BaseDomainService or UnitTestBase
    }
}

as you can see it’s just simplest implementation of Abstract Singleton and it can be easily substituted for our testing purposes. For “real” environment we can assign it here

Code Snippet
/// <summary>
/// Provides base repository and transactional support + common entity processing before save
/// </summary>
/// <typeparam name="T">ObjectContext</typeparam>
public abstract class DomainServiceBase<T> : LinqToEntitiesDomainService<T>
    where T : ObjectContext, IObjectContext, new()
{
    public override void Initialize(DomainServiceContext context)
    {
        base.Initialize(context);
        DataContext.Current = ObjectContext;
    }

For “test” environment here

Code Snippet
[TestClass]
public abstract class UnitTestBase
{
    [TestInitialize]
    public void MyTestInitialize()
    {
        DataContext.Current = PrepareDataContext();
    }

    protected abstract IObjectContext PrepareDataContext();
}

the last thing we need to do is to implement “test” versions of our main EF related abstractions IObjectSet and IObjectContext

Code Snippet
public class FakeObjectSet<TEntity> : IObjectSet<TEntity> where TEntity : class
{
    private readonly HashSet<TEntity> _data;
    private readonly IQueryable _query;

    public FakeObjectSet() : this(new List<TEntity>())
    {
    }

    public FakeObjectSet(IEnumerable<TEntity> testData)
    {
        _data = new HashSet<TEntity>(testData);
        _query = _data.AsQueryable();
    }

    #region IObjectSet<TEntity> Members

    public void AddObject(TEntity item)
    {
        _data.Add(item);
    }

    public void DeleteObject(TEntity item)
    {
        _data.Remove(item);
    }

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

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

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

    IEnumerator<TEntity> IEnumerable<TEntity>.GetEnumerator()
    {
        return _data.GetEnumerator();
    }

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

    Expression IQueryable.Expression
    {
        get { return _query.Expression; }
    }

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

    #endregion
}

the code speaks for itself – our FakeObjectSet is just banal wrapper around HashSet. Next comes the sweet FakeDb

Code Snippet
public class FakeDb : IObjectContext
{
    protected Dictionary<Type, object> Sets = new Dictionary<Type, object>();

    public void AddEntity<T>(T entity) where T : EntityObject
    {
        if (entity == null) return;

        CreateObjectSet<T>().Attach(entity);
    }

    public void AddEntities<T>(IEnumerable<T> entities) where T : EntityObject
    {
        if (entities == null) return;

        foreach (var entity in entities)
        {
            AddEntity(entity);
        }
    }

    public IObjectSet<T> CreateObjectSet<T>() where T : class
    {
        object set;
        if (!Sets.TryGetValue(typeof(T), out set))
        {
            set = new FakeObjectSet<T>();
            Sets.Add(typeof(T), set);
        }
        return (FakeObjectSet<T>)set;
    }

    // override to provide some real DB like behaviour (ex: IDs generation)
    public virtual int SaveChanges()
    {
        return 1; // what a surprise ;)
    }

    public void Clear()
    {
        Sets.Clear();
    }
}

again – simple, straightforward implementation – our FakeDB is just a set of FakeObjectSets or “in memory DB-like structure” for simplicity. Well, so far so good, let’s write our first Unit Test

Code Snippet
[TestClass]
public class UnitTestExample : UnitTestBase
{
    protected override IObjectContext PrepareDataContext()
    {
        // for integration-style UT with real DB
        // return new TestEntities();
        var db = new FakeDb();
        db.AddEntity(new Order { Name = "Test" });
        db.AddEntity(new Order { Name = "Test1" });
        return db;
    }

    [TestMethod]
    public void TestMethod1()
    {
        var ordersRepository = new RepositoryBase<Order>();
        Assert.AreEqual(2, ordersRepository.Query.Count());
        var order = ordersRepository.Query.Where(c => c.Name == "Test").SingleOrDefault();
        Assert.IsNotNull(order);
    }
}

Great result – with the minimum amount of plumbing we substituted our real DB with in-memory twin that we can fill with test data we need. And if you prefer integration-style unit test with real test DB, you can easily do that as well. And of course all standard CRUD will work ;)

1 comment:

  1. Sergey,

    Great examples. I'm understanding and starting to implement for testing in my project now. I only have had one hangup - "UpdateHelper.UpdateCommonFields(entity)"

    I don't see a reference to that anywhere, is that just a stub so "common fields" (like maybe those used for metadata concurrency) are tracked and updated?
    If so that would make sense - although do you have your example so I could see what you were intending with that?

    Thx... Adam

    ReplyDelete