Mock IRavenQueryable с добавлением выражения Where()

Я пытаюсь сделать базовое доказательство кода типа концепции для нового проекта mvc3. Мы используем Moq с RavenDB.

Действие:

public ActionResult Index(string id)
{
    var model = DocumentSession.Query<FinancialTransaction>()
        .Where(f => f.ResponsibleBusinessId == id);
    return View(model);
}

Тест:

private readonly Fixture _fixture = new Fixture();

[Test]
public void Index_Action_Returns_List_Of_FinancialTransactions_For_Business([Random(0, 50, 5)]int numberOfTransactionsToCreate)
{
    // Arrange
    var session = new Mock<IDocumentSession>();
    var financialController = new FinancialController { DocumentSession = session.Object };

    var businessId = _fixture.CreateAnonymous<string>();
    var transactions = _fixture.Build<FinancialTransaction>()
        .With(f => f.ResponsibleBusinessId, businessId)
        .CreateMany(numberOfTransactionsToCreate);

    // Mock
    var ravenQueryableMock = new Mock<IRavenQueryable<FinancialTransaction>>();
    ravenQueryableMock.Setup(x => x.GetEnumerator()).Returns(transactions.GetEnumerator);
    ravenQueryableMock.Setup(x => x.Customize(It.IsAny<Action<Object>>()).GetEnumerator()).Returns(() => transactions.GetEnumerator());

    session.Setup(s => s.Query<FinancialTransaction>()).Returns(ravenQueryableMock.Object).Verifiable(); 

    // Act
    var actual = financialController.Index(businessId) as ViewResult;

    // Assert
    Assert.IsNotNull(actual);
    Assert.That(actual.Model, Is.InstanceOf<List<FinancialTransaction>>());

    var result = actual.Model as List<FinancialTransaction>;
    Assert.That(result.Count, Is.EqualTo(numberOfTransactionsToCreate));
    session.VerifyAll();
}

Казалось бы, проблема заключается в .Where(f = > f.ResponsibleBusinessId == id). Из издевавшегося IRavenQueryable я возвращаю список FinancialTransactions, поэтому можно подумать, что фильтр Where() будет фильтроваться на основе этого. Но поскольку это IQueryable, я предполагаю, что он пытается выполнить выражение все как одно, когда оно перечисляет.

Чтобы проверить, я изменил запрос на действие:

var model = DocumentSession.Query<FinancialTransaction>()
    .ToList()
    .Where(f => f.ResponsibleBusinessId == id);

Это позволяет пройти тест, однако он не идеален, поскольку это означает, что он будет перечислять все записи, а затем фильтровать их.

Есть ли способ заставить Moq работать с этим?

Ответ 1

Как уже упоминалось в комментариях, вы не должны издеваться над API RavenDB в своих тестах.

RavenDB имеет отличную поддержку для модульного тестирования благодаря режиму InMemory:

[Test]
public void MyTest()
{
    using (var documentStore = new EmbeddableDocumentStore { RunInMemory = true })
    {
        documentStore.Initialize();

        using (var session = documentStore.OpenSession())
        {
            // test
        }
    }
}

Ответ 2

Как уже упоминалось, если вы можете справиться с встроенным в память/встроенным режимом, это отлично подходит для тестирования интеграции. Но, по моему мнению, это не так просто или легко для модульного тестирования.

Я нашел сообщение блога от Сэма Ритчи, которое предлагает "подделку" (обертка вокруг стандартного LINQ IQueryable) для IRavenQueryable для таких случаях. Он немного устарел, поскольку более новые версии Raven (в настоящее время 2.5) предлагают несколько дополнительных методов на интерфейсе IRavenQueryable. В настоящее время я не использую эти новые методы (TransformWith, AddQueryInput, Spatial), поэтому я просто лениво оставил NotImplementedException в приведенном ниже коде.

См. сообщение Сэма для исходного кода, на котором я основывался, и для примеров использования.

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

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

    public RavenQueryStatistics QueryStatistics { get; set; }

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

    public Expression Expression {
        get { return this.source.Expression; }
    }

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

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

    public IRavenQueryable<TResult> TransformWith<TTransformer, TResult>() where TTransformer : AbstractTransformerCreationTask, new() {
        throw new NotImplementedException();
    }

    public IRavenQueryable<T> AddQueryInput(string name, RavenJToken value) {
        throw new NotImplementedException();
    }

    public IRavenQueryable<T> Spatial(Expression<Func<T, object>> path, Func<SpatialCriteriaFactory, SpatialCriteria> clause) {
        throw new NotImplementedException();
    }

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

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

    IEnumerator IEnumerable.GetEnumerator() {
        return this.source.GetEnumerator();
    }
}

public class FakeRavenQueryProvider : IQueryProvider {
    private readonly IQueryable source;

    private readonly RavenQueryStatistics stats;

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

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

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

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

    public object Execute(Expression expression) {
        return this.source.Provider.Execute(expression);
    }
}