FakeRavenQueryable<T>

November 28, 2012 - ravendb unit-testing

We were recently trying to build basic unit tests for the controller actions on an MVC4 + RavenDB application, and having problems attempting to mock the IDocumentSession. The RavenDB people consistently say not to do that, and that running an EmbeddedDocumentStore solves every unit test problem under the sun and then makes you breakfast. However, we tried it and weren’t really happy with the code-to-value ratio.

The process is typically:

  1. Create an EmbeddedDocumentStore
  2. Create all your indexes
  3. Create a session, load your test document set, and save changes
  4. Wait for indexing to complete (eg by registering a custom IQueryListener that modifies all queries to wait for non-stale results)
  5. Create & inject your session
  6. Run your tests

This approach requires a lot of setup, the tests are slow, and the package dependencies on your test project are considerable, where all we really wanted to accomplish was to return a specific result set in response to a specific method call on the IDocumentSession.

The most immediate problem you run into when mocking IDocumentSession is returning a useable IRavenQueryable<T>. In case anyone else is brave enough to risk the scorn of Ayende, below is my implementation of a FakeRavenQueryable<T> class that wraps a generic IQueryable<T>:

public class FakeRavenQueryable<T> : IRavenQueryable<T> { 
  private IQueryable<T> source;

  public RavenQueryStatistics QueryStatistics { get; set; }

  public FakeRavenQueryable(IQueryable<T> source, RavenQueryStatistics stats = null) { 
    this.source = source; 
    QueryStatistics = stats;
  }

  public IRavenQueryable<T> Customize(Action<Raven.Client.IDocumentQueryCustomization> action) { 
    return this; 
  }

  public IRavenQueryable<T> Statistics(out RavenQueryStatistics stats) { 
    stats = QueryStatistics; 
    return this; 
  }

  public IEnumerator<T> GetEnumerator() { 
    return source.GetEnumerator(); 
  }

  System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() { 
    return source.GetEnumerator(); 
  }

  public Type ElementType { 
    get { 
      return typeof(T); 
    } 
  }

  public System.Linq.Expressions.Expression Expression { 
    get { 
      return source.Expression; 
    } 
  }

  public IQueryProvider Provider { 
    get { 
      return new FakeRavenQueryProvider(source, QueryStatistics); 
    } 
  } 
}

public class FakeRavenQueryProvider : IQueryProvider { 
  private IQueryable source; 
  private RavenQueryStatistics stats;

  public FakeRavenQueryProvider(IQueryable source, RavenQueryStatistics stats = null) { 
    this.source = source; 
    this.stats = stats; 
  }

  public IQueryable<TElement> CreateQuery<TElement>(System.Linq.Expressions.Expression expression) { 
    return new FakeRavenQueryable<TElement>(source.Provider.CreateQuery<TElement>(expression), stats); 
  }

  public IQueryable CreateQuery(System.Linq.Expressions.Expression expression) {
    var type = typeof(FakeRavenQueryable<>).MakeGenericType(expression.Type); 
    return (IQueryable)Activator.CreateInstance(type, source.Provider.CreateQuery(expression), stats); 
  }

  public TResult Execute<TResult>(System.Linq.Expressions.Expression expression) { 
    return source.Provider.Execute<TResult>(expression); 
  }

  public object Execute(System.Linq.Expressions.Expression expression) { 
    return source.Provider.Execute(expression); 
  }
}

It can be returned from a mocked IDocumentSession using code like the following (using Moq in this case):

Mock<IDocumentSession> session = new Mock<IDocumentSession>(); 
session.Setup(s => s.Query<Product>()).Returns( 
  new FakeRavenQueryable<Product>(productList.AsQueryable(), new RavenQueryStatistics { TotalResults = 100 } ) 
); 

// inject your session & run your tests here 
RavenQueryStatistics stats; 
var results = session.Object.Query<Product>()
  .Where(p => p.Id == "Products/1")
  .Statistics(out stats)
  .Take(1)
  .ToList();

It won’t make you breakfast, give you full access to the advanced session methods, or tell you if you’re passing unsupported expressions, but it will allow you to mock out simple queries in your application, and the Linq methods all work over your test list as you’d expect (even In!)