Testable Data Access With The Repository Pattern

14 בנובמבר 2008

In my last post I explored a little about how we use the Entity Framework. One question that comes up a lot is how do you test your services with the data access layer without hitting the database. Not hitting the database during tests is not only a performance issue. Unless you build and tear down your data on every run you have to make sure the test data is consistent. So how do we do it?


The first step is to create an in-memory version of the repository. This implementation will keep data in memory instead of hitting the database. It is important to build this in-memory version early and evolve it with the real repository. By having an in-memory implementation of the repository you immediately get a warning flag if you try to exploit some esoteric feature of your ORM or other data access technology. Not that it is inherently bad to exploit such features, but data access technologies come and go so this should be done carefully. (L2S anyone?)


Note that the implementation below is not only for unit tests. You can inject this implementation instead of a real one in the application to run it with consistent data.


Here’s a refresher of the interface.







public interface IRepository : IDisposable


{


    T[] GetAll<T>();


    T[] GetAll<T>(Expression<Func<T, bool>> filter);


    T GetSingle<T>(Expression<Func<T, bool>> filter);


    T GetSingle<T>(Expression<Func<T, bool>> filter, List<Expression<Func<T, object>>> subSelectors);


    void Delete<T>(T entity);


    void Add<T>(T entity);


    int SaveChanges();


    DbTransaction BeginTransaction();


}


Now let’s jump to the incredibly complex implementation of the in-memory implementation of the repository. :-)

As you can see below, the code to implement a in-memory repository is remarkably slim. Despite the fact that the code is not rocket science, let’s walk through some interesting parts. The filters are all implemented as Expressions<>. In the in-memory implementation we don’t need to do anything with the expressions, so we compile them and pass them as specifications to Linq to Objects.
Another interesting point (IMHO) is the fact that it has become so simple to work with types. You just just call .OfType<> and pass in the type you want and Linq does the rest. 
The last point I would like to emphasize, is the use of a NullTransaction. We don’t need transactions for our in-memory implementation, but the service code might call any of the methods on the DbTransaction object returned from BeginTransaction. By the use of a NullTransaction (See code below) we simulate transactions. (I guess you could use a mocking framework for some of this, but I’m not there yet…)


Let’s dissect how this works:



public T GetSingle<T>(Expression<System.Func<T, bool>> filter)


{


    var predicate = filter.Compile();


    return _storage.OfType<T>().Where(p => predicate(p)).FirstOrDefault();


}


The first step is to compile the expression so that we end up with a predicate that we use in our Linq query. In the next line we first specify the type T, which was passed in by the caller so we just pass that on. Next we call OfType<T> to filter on only the requested type and then we filter using Where() passing in our predicate. We then return to the caller either the first instance found or nothing. That’s it.


Here’s the full implementation. (Note: The SaveChanges() just returns 1. If you have service logic that depends on the actual return value you will have to track changes yourself.)



public class InMemoryRepository : IRepository


{


    private List<object> _storage = new List<object>();


    #region IRepository Members


    public T[] GetAll<T>()


    {


        return _storage.OfType<T>().ToArray();


    }


    public T[] GetAll<T>(Expression<System.Func<T, bool>> filter)


    {


        var predicate = filter.Compile();


        return _storage.OfType<T>().Where(p => predicate(p)).ToArray();


    }


    public T GetSingle<T>(Expression<System.Func<T, bool>> filter)


    {


        var predicate = filter.Compile();


        return _storage.OfType<T>().Where(p => predicate(p)).FirstOrDefault();


    }


    public T GetSingle<T>(Expression<System.Func<T, bool>> filter, List<Expression<System.Func<T, object>>> subSelectors)


    {


        // no need for sub selectors in L2O.


        return GetSingle<T>(filter);


    }


    public void Delete<T>(T entity)


    {


        _storage.Remove(entity);


    }


    public void Add<T>(T entity)


    {


        _storage.Add(entity);


    }


    public int SaveChanges()


    {


        return 1;


    }


    public DbTransaction BeginTransaction()


    {


        return new NullTransaction();


    }


    #endregion


    #region IDisposable Members


    public void Dispose()


    {


        // nothing to dispose here


    }


    #endregion


}


Here is the implementation of the NullTransaction



public class NullTransaction : DbTransaction


{


    protected override DbConnection DbConnection


    {


        get { throw new NotImplementedException(); }


    }


    public override IsolationLevel IsolationLevel


    {


        get { throw new NotImplementedException(); }


    }


    public override void Commit()


    {


        // do nothing


    }


    protected override void Dispose(bool disposing)


    {


        base.Dispose(disposing);


    }


    public override void Rollback()


    {


        // do nothing


    }


}


Now you can easily pre-populate the in-memory repository with consistent data so that you can test your application code easier.

kick it on DotNetKicks.com

Add comment
facebook linkedin twitter email

Leave a Reply

Your email address will not be published.

*

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>

5 comments

  1. Benjamin Eidelman17 בנובמבר 2008 ב 9:25

    Hi Kim,

    I'm not very experienced in Mock Objets, so I have some questions:
    - If you are exposing methods as GetSingle that uses Expression<> as a filter parameter, why not just expose an IQueryable (allowing OrderBy, Select, and other Linq Operators), apparently, the life of this interface is going to last in the .Net Framework

    - Shouldn't you apply changes only on SaveChanges()?, for e.g. adding a list of pending changes, that way this would produce the expected result (no insert):

    IRepository rep = new InMemoryRepository();
    rep.Add(new Product());
    rep.DiscardChanges();

    even further, forgetting to call the SaveChanges() method won't be covered by the unit tests.

    Maybe you intentionally omit this for simplicity?

    ps: just an observation, the result of FirstOrDefault() is not always the same as SingleOrDefault()

    Kind Regards,

    Reply
  2. Kim18 בנובמבר 2008 ב 4:47

    @Benjamin,
    IQueryable – The short answer is in the comments on this post. The long answer will have to wait…

    Apply changes (pending list) – Didnt' have a need for it. I never discard changes to a repository. If I don't want them I just never commit the transaction.

    Forgetting to call SaveChanges – True, Maybe this is a deficiency in my testing strategy. I have to think about it…

    ps: SingleOrDefault() – Had to look that one up. Thanks. BTW, Entity Framework does not support SingleXXX(). (Another reason to hide implementation in the repository)

    Reply
  3. DeniseHanson13 במאי 2011 ב 5:41

    Following my own investigation, thousands of persons all over the world get the loan from different creditors. Therefore, there is good possibilities to get a consolidation loan in all countries.

    Reply
  4. Suiplieni5 בפברואר 2013 ב 6:55

    Insightful post here. Thanks Steve for this necessary and valuable post.
    Marko

    Reply
  5. zazzle discount8 במאי 2013 ב 23:22

    Please forgive my English.Heya i’m for the first time here. I came across this board and I find It really useful & it helped me out much. I hope to give something back and aid others like you helped me.

    Reply