14

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

在很多情况下,在我的服务类中编码方法时,我使用的是FindFirstandFind方法。如您所见,它们都将 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

任何指导将不胜感激。

4

4 回答 4

20

非常简短的回答是否定的,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 接口并引入 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 接口是否让您的生活更轻松。你可以尝试编写你想要的测试然后看看你可以推出什么样的设计来支持这种可测试性。

希望这可以帮助。

于 2011-04-14T05:16:13.833 回答
8

当我试图弄清楚如何使用 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);
}
于 2011-06-13T18:43:30.643 回答
4

有一种方法可以做到这一点,通过比较 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..)

仅当.Find()使用表达式调用模拟存储库时i => !i.IsProcessed && i.IsConfirmed,它才会返回中指定的内容.Returns()

于 2019-03-28T18:22:52.897 回答
3

我不愿意放弃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();
        }
    }
}
于 2014-09-30T18:22:55.713 回答