NSubstitute-测试特定的linq表达式

|| 我在当前正在开发的MVC 3应用程序中使用存储库模式。我的存储库界面如下所示:
public interface IRepository<TEntity> where TEntity : IdEntity
{
    void Add(TEntity entity);
    void Update(TEntity entity);
    void Remove(TEntity entity);
    TEntity GetById(int id);
    IList<TEntity> GetAll();
    TEntity FindFirst(Expression<Func<TEntity, bool>> criteria);
    IList<TEntity> Find(Expression<Func<TEntity, bool>> criteria);
}
在很多情况下,在服务类中对方法进行编码时,我使用的是
FindFirst
Find
方法。如您所见,它们都使用linq表达式作为输入。我想知道的是NSubstitute是否允许您在代码中指定要测试的特定表达式。 因此,这是一个服务方法的示例,它说明了我提到的存储库方法之一的用法:
public IList<InvoiceDTO> GetUnprocessedInvoices()
{
    try
    {
        var invoices = _invoiceRepository.Find(i => !i.IsProcessed && i.IsConfirmed);
        var dtoInvoices = Mapper.Map<IList<Invoice>, IList<InvoiceDTO>>(invoices);
        return dtoInvoices;
    }
    catch (Exception ex)
    {
        throw new Exception(string.Format(\"Failed to get unprocessed invoices: {0}\", ex.Message), ex);
    }
}
因此,有没有一种方法可以使用NSubtitute测试特定的lamda表达式:
i => !i.IsProcessed && i.IsConfirmed
? 任何指导将不胜感激。     
已邀请:
简短的回答是:否,NSubstitute没有构建任何东西来简化对特定表达式的测试。 更长的答案是,您可以尝试一些选项,其中大多数选项都涉及避免在被测类中直接使用LINQ。我不确定其中的任何一个好主意,因为我不了解完整的上下文,但是希望这里会有一些可用的信息。在以下示例中,我取消了Mapper步骤,以使代码样本小一些。 第一种选择是制作它,以便您可以检查表达式与期望的引用是否相同,这意味着您不再可以直接在被测代码中创建它。例如:
//Class under test uses:
_invoiceRepository.Find(Queries.UnprocessedConfirmedOrders)

[Test]
public void TestUnprocessedInvoices()
{
    IList<InvoiceDTO> expectedResults = new List<InvoiceDTO>();
    _invoiceRepository.Find(Queries.UnprocessedConfirmedOrders).Returns(expectedResults);
    Assert.That(_sut.GetUnprocessedInvoices(), Is.SameAs(expectedResults));
}
我已经将表达式转储到静态Queries类中,但是您可以使用工厂更好地对其进行封装。因为您有对所使用的实际表达式的引用,所以可以设置返回值并检查是否正常接收到调用。您还可以单独测试表达式。 第二种选择通过使用规范模式使这一点更进一步。假设您将以下成员添加到IRepository接口,并引入了ISpecification:
public interface IRepository<TEntity> where TEntity : IdEntity
{
   /* ...snip... */
    IList<TEntity> Find(ISpecification<TEntity> query);
}

public interface ISpecification<T> { bool Matches(T item);  }
然后可以像这样测试它:
//Class under test now uses:
_invoiceRepository.Find(new UnprocessedConfirmedOrdersQuery());

[Test]
public void TestUnprocessedInvoicesUsingSpecification()
{
    IList<InvoiceDTO> expectedResults = new List<InvoiceDTO>();
    _invoiceRepository.Find(Arg.Any<UnprocessedConfirmedOrdersQuery>()).Returns(expectedResults);
    Assert.That(_sut.GetUnprocessedInvoices(), Is.SameAs(expectedResults));
}
同样,您可以单独测试此查询,以确保它符合您的想法。 第三种选择是捕获使用的参数并直接对其进行测试。这有点混乱,但可以:
[Test]
public void TestUnprocessedInvoicesByCatchingExpression()
{
    Expression<Func<InvoiceDTO, bool>> queryUsed = null;
    IList<InvoiceDTO> expectedResults = new List<InvoiceDTO>();
    _invoiceRepository
        .Find(i => true)
        .ReturnsForAnyArgs(x =>
        {
            queryUsed = (Expression<Func<InvoiceDTO, bool>>)x[0];
            return expectedResults;
        });

    Assert.That(_sut.GetUnprocessedInvoices(), Is.SameAs(expectedResults));
    AssertQueryPassesFor(queryUsed, new InvoiceDTO { IsProcessed = false, IsConfirmed = true });
    AssertQueryFailsFor(queryUsed, new InvoiceDTO { IsProcessed = true, IsConfirmed = true });
}
(希望这将在将来的NSubstitute版本中变得更加容易) 第四个选择是查找/借用/写入/窃取一些可以比较表达式树的代码,并使用NSubstitute \的Arg.Is(...),该谓词可以在此处比较表达式树。 第五个选择是不对它进行单元测试,仅使用真实的InvoiceRepository进行集成测试。不必担心发生的事情的机理,而是尝试验证所需的实际行为。 我的一般建议是准确地查看您需要测试的内容以及如何最好,最轻松地编写这些测试。请记住,表达式和传递的事实都需要进行某种方式的测试,并且测试不必是单元测试。还可能需要考虑当前的IRepository接口是否使您的生活更轻松。您可以尝试编写所需的测试,然后查看可以采用哪些设计来支持该可测试性。 希望这可以帮助。     
当我试图弄清楚如何使用NSubstitute中的lambda表达式返回特定值时,我偶然发现了这个问题。但是,对于我的用例,我不在乎linq查询中实际传递了什么,而是想分享如何在NSubstitute中的模拟接口上返回linq查询的值。 所以使用上面的例子
[Test]
public void TestUnprocessedInvoices()
{
    IList<InvoiceDTO> expectedResults = new List<InvoiceDTO>();
    _invoiceRepository.Find(Arg.Any<Expression<Func<Invoice, bool>>>()).Returns(expectedResults);
}
    
我不愿意在存储库界面中放弃使用
Expression<Func<T,bool>>
,因此,除了编写一个特定的模拟程序(因为NSubstitute不支持它)之外,我还只是在测试装置中创建了一个私有类来实现该模拟,存储库接口,以及仅测试将使用的与表达式相关的方法。我能够像往常一样继续使用NSubstitute来模拟所有其他依赖项,但是我可以将该同一个存储库用于几个不同的测试,并实际上从不同的输入中获得不同的结果。
public class SomeFixture
{
    private readonly IRepository<SomeEntity> entityRepository;
    private readonly IRepository<SomeThing> thingRepository;

    public SomeFixture()
    {
        var entities = new List<SomeEntity>
        {
            BuildEntityForThing(1),
            BuildEntityForThing(1),
            BuildEntityForThing(1),
            BuildEntityForThing(2),
        };
        entityRepository = new FakeRepository(entities);

        thingRepository = Substitute.For<IRepository<SomeThing>>();
        thingRepository.GetById(1).Returns(BuildThing(1));
        thingRepository.GetById(2).Returns(BuildThing(2));
    }

    public void SomeTest()
    {
        var classUnderTest = new SomeClass(thingRepository, entityRepository);

        Assert.AreEqual(classUnderTest.FetchEntitiesForThing(1).Count, 3);
    }

    private void SomeOtherTest()
    {
        var classUnderTest = new SomeClass(thingRepository, entityRepository);

        Assert.AreEqual(classUnderTest.FetchEntitiesForThing(2).Count, 1);
    }

    private class FakeRepository : IRepository<SomeEntity>
    {
        private readonly List<SomeEntity> items;

        public FakeRepository(List<SomeEntity> items)
        {
            this.items = items;
        }

        IList<TEntity> Find(Expression<Func<SomeEntity, bool>> criteria)
        {
            // For these purposes, ignore possible inconsistencies 
            // between Linq and SQL when executing expressions
            return items.Where(criteria.Compile()).ToList();
        }

        // Other unimplemented methods from IRepository ...
        void Add(SomeEntity entity)
        {
            throw new NotImplementedException();
        }
    }
}
    
有一种方法可以通过比较lambda表达式是否相等来实现。在此处针对相关问题编写了一个非常受欢迎的答案,其中给出了LambdaCompare类的示例。 然后,您可以使用此LambdaCompare在模拟设置中检查表达式或lambda是否相等:
var mockRepository = Substitute.For<IRepository>();
mockRepository.Find(Arg.Is<Expression<Func<Invoice, bool>>>(expr =>
                    LambdaCompare.Eq(expr, i => !i.IsProcessed && i.IsConfirmed))
              .Returns(..etc..)
仅当用表达式
i => !i.IsProcessed && i.IsConfirmed
调用模拟存储库
.Find()
时,它才会返回
.Returns()
中指定的内容。     

要回复问题请先登录注册