问题 使用附加的Where()表达式模拟IRavenQueryable


我正在尝试为新的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使用它?


7463
2018-04-12 19:05


起源

作为一个侧面想法,您是否考虑过使用嵌入式内存版本而不是嘲笑RavenDB?使用Linq扩展方法进行模拟(实际上)非常糟糕,我使用RavenDB因为没有真正需要模拟数据库。 - Rangoric
我和@Rangoric在一起 - 没有必要嘲笑它 IDocumentSession 和 IDocumentStore (或嘲笑任何Ravendb)当RavenDb有一个 EmbeddedDocumentStore。伙计,跳进去 jabbr.net/#/rooms/RavenDB 我们都会和你聊天为什么/为什么不呢。 - Pure.Krome
这很好 - 除了我看到RavenDB需要很长时间才能启动我的单元测试 - 说“它的设计不被嘲笑”并不是正确的答案。 - Ronnie


答案:


正如评论中所提到的,您不应该在测试中模拟RavenDB API。

由于InMemory模式,RavenDB对单元测试提供了出色的支持:

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

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

8
2018-04-13 09:05



在我的机器上加载和初始化Raven嵌入式服务器需要将近3秒钟。这是一个不可接受的时间来添加单元测试。 - Mike Scott
@MikeScott 3秒听起来不对劲。您的机器有哪些规格? - Arnold Zokas
你需要多长时间?您是否在单元测试中对其进行了观察,包括运行索引创建任务?搭载Intel Core 2 Duo E4600的Win7 / 64,2.4GHz,4GB RAM。 - Mike Scott
实际初始化是亚秒级。然而,我注意到,有时会出现与初始化复杂索引相关的高成本。您的硬件看起来应该处理这个问题。 - Arnold Zokas
只是为了确认:你是在内存中初始化它,对吗?我相信你是 - 我只是想排除明显的。 - Arnold Zokas


正如其他人所提到的,如果你可以使用内存/嵌入模式,那就很棒了 积分 测试。但在我看来,单元测试并不快或不够快。

我发现了一个 博客文章 由Sam Ritchie提供的“假”(包装在标准LINQ周围) IQueryable)对于像这样的情况的IRavenQueryable。他有点过时,因为新版本的Raven(目前为2.5)提供了一些额外的方法 IRavenQueryable 接口。我目前不使用这些新方法(TransformWithAddQueryInputSpatial),所以我懒得离开 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);
    }
}

3
2018-04-14 16:57



实际上是集成测试的单元测试是一个真正的问题。单元测试应在尽可能短的时间内运行,以便长时间保持可用和使用。 - utunga
@utunga我不确定你想说什么。如果你想开始讨论单元测试和集成测试,那应该是在chatmers.stackexchange.com的聊天或问题中。如果这不是您所说的,并且具体与此答案有关,请澄清。 - Jon Adams
对不起......我实际上要说的是“这个答案应该在顶部,因为真正的单元测试应该设计为在短时间内运行......”。不要进入整个单元测试与集成测试的事情(并注意到Ayende似乎已经将他的位置转向集成测试,远离单元测试加上模拟或假货)但是我看着它,而我对人们很好在这方面做任何他们想做的事情,问题是询问Moq因此该人正在采用(真正的)单元测试方法 - 所以你的答案应该是首选的(即使它是假的而不是模拟)。 - utunga
值得注意的是,Sam Ritchie的基于LINQ的FakeRavenQueryable不再与RavenDB.Client中的最新IRavenQueryable接口保持同步,这有点让人感到羞耻(尽管在大多数情况下可能只是添加一堆NotImplemented异常可能无害) - utunga
另外注意,执行 IQueryProvider 如果您使用的是异步api,则无效。 Raven扩展方法如 .ToListAsync() 抱怨查询提供者不是一个 IRavenQueryProvider - simonlchilds