NSubstitute -测试特定的linq表达式

本文关键字:linq 表达式 测试 NSubstitute | 更新日期: 2023-09-27 17:49:58

我正在开发的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);
}

在很多情况下,当在我的服务类中编码方法时,我使用FindFirstFind方法。正如您所看到的,它们都以linq表达式作为输入。我想知道的是是否有一种方法NSubstitute允许您指定您想要在代码中测试的特定表达式。

那么,这里有一个服务方法的例子,它说明了我提到的Repository方法之一的使用:

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);
    }
}

那么,是否有一种方法,使用nsubstitute,我可以测试特定的lambda表达式:i => !i.IsProcessed && i.IsConfirmed ?

NSubstitute -测试特定的linq表达式

简短的回答是否定的,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));
}

我已经将表达式转储到一个静态查询类中,但是您可以使用工厂来更好地封装它。由于您有对所使用的实际表达式的引用,因此可以设置返回值并检查是否正常接收了调用。还可以单独测试表达式。

第二个选项通过使用规范模式进一步扩展了这一点。假设您将以下成员添加到IRepository接口并引入isspecification:

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);
}

有一种方法可以做到这一点,通过比较lambda表达式是否相等。一个非常受欢迎的答案是针对这里的一个相关问题编写的,它给出了一个lambdacomcompare类的示例。

然后可以使用这个lambdacomare来检查模拟设置中的表达式或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()

中指定的内容。

我不愿意放弃在我的存储库接口中使用Expression<Func<T,bool>>,因此作为编程这个特定模拟的替代方法(因为NSubstitute不支持它),我只是在我的测试fixture中创建了一个私有类,它实现了我的存储库接口,并且只使用测试将要使用的与expression相关的方法。我能够像往常一样继续使用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();
        }
    }
}