将 Expression<Func<T, bool>> 作为参数传入的 Moq'ing 方法

Posted

技术标签:

【中文标题】将 Expression<Func<T, bool>> 作为参数传入的 Moq\'ing 方法【英文标题】:Moq'ing methods where Expression<Func<T, bool>> are passed in as parameters将 Expression<Func<T, bool>> 作为参数传入的 Moq'ing 方法 【发布时间】:2011-07-08 23:23:40 【问题描述】:

我对单元测试和模拟非常陌生!我正在尝试编写一些单元测试来涵盖一些与数据存储交互的代码。数据访问被IRepository封装:

interface IRepository<T> 
    ....
    IEnumerable<T> FindBy(Expression<Func<T, bool>> predicate);
    ....

我尝试使用 IRepository 的具体 IoC 实现来测试的代码如下所示:

public class SignupLogic 
    private Repository<Company> repo = new Repository<Company>();

    public void AddNewCompany(Company toAdd) 
        Company existingCompany = this.repo.FindBy(c => c.Name == toAdd.Name).FirstOrDefault();

        if(existingCompany != null) 
            throw new ArgumentException("Company already exists");
        

        repo.Add(Company);
        repo.Save();
    

所以我正在测试 SignupLogic.AddNewCompany() 本身的逻辑,而不是逻辑和具体的存储库,我正在模拟 IRepository 并将其传递给 SignupLogic。模拟的存储库如下所示:

Mock<Repository> repoMock = new Mock<Repository>();
repoMock.Setup(moq => moq.FindBy(c => c.Name == "Company Inc")....

它返回一个内存中的 IEnumberable,其中包含名称设置为“Company Inc”的 Company 对象。调用 SignupLogic.AddNewCompany 的单元测试设置了一个具有重复详细信息的公司并尝试将其传递进来,我断言抛出了 ArgumentException 并显示消息“公司已存在”。此测试未通过。

在运行时通过单元测试和 AddNewCompany() 进行调试,existingCompany 似乎始终为空。无奈之下,我发现如果我更新 SignupLogic.AddNewCompany() 以便对 FindBy 的调用如下所示:

Company existingCompany = this.repo.FindBy(c => c.Name == "Company Inc").FirstOrDefault();

测试通过,这表明 Moq 仅响应与我在测试夹具中设置的代码完全相同。显然,这在测试任何重复的公司是否被 SignupLogic.AddNewCompany 拒绝时并不是特别有用。

我尝试将 moq.FindBy(...) 设置为使用“Is.ItAny”,但这也不会导致测试通过。

从我正在阅读的所有内容来看,似乎我试图在此处使用 Moq 测试表达式实际上是不可行的。可能吗?请帮忙!

【问题讨论】:

如果你的目标是只测试返回,你可以强制你的 Mock 框架忽略这个参数: Mock.Arrange(() => repo.AllIncluding(x => x.Category == category )) .IgnoreArguments() .Returns((new Listnew Product()) .AsQueryable());在上面的示例中,我使用的是 JustMock 的免费版本。 【参考方案1】:

只有具有完全相同结构(和文字值)的Expression 才会匹配,这可能是正确的。我建议你使用 Returns() 的重载,它可以让你使用调用 mock 的参数:

repoMock.Setup(moq => moq.FindBy(It.IsAny<Expression<Func<Company, bool>>>())
        .Returns((Expression<Func<Company, bool>> predicate) => ...);

... 中,您可以使用predicate 返回匹配的公司(如果匹配的公司不是您所期望的,甚至可能抛出异常)。不是很漂亮,但我认为它会起作用。

【讨论】:

这让我至少可以为 UniqueCompanyAccepted 和 DuplicateCompanyRejected 测试获得一些绿灯 :) 这正是我所追求的,但“...”部分是否有可能根据您在 Aasmund 上方的答案中的 cmets 进一步扩展???即实际上尝试匹配传递到表达式中的公司? @IbrarMumtaz:如果您有一个包含所有公司的列表companies,那么.Returns((Expression&lt;Func&lt;Company, bool&gt;&gt; predicate) =&gt; companies.Where(predicate)); 应该可以工作。但是,如果您需要比这更高级的功能,最好编写一个实现 IRepository&lt;Company&gt; 的类,而不是使用 Moq,因为将复杂的行为构建到模拟中很麻烦。【参考方案2】:

您应该能够使用It.IsAny&lt;&gt;() 来完成您想做的事情。使用It.IsAny&lt;&gt;(),您只需调整设置的返回类型即可测试代码的每个分支。

It.IsAny<Expression<Func<Company, bool>>>()

第一次测试,返回一个公司,不管会导致异常抛出的谓词:

var repoMock = new Mock<IRepository<Company>>();
repoMock.Setup(moq => moq.FindBy(It.IsAny<Expression<Func<Company, bool>>>())).Returns(new List<Company>new CompanyName = "Company Inc");
var signupLogic = new SignupLogic(repoMock.Object);
signupLogic.AddNewCompany(new Company Name = "Company Inc");
//Assert the exception was thrown.

第二次测试,将返回类型设为空列表,这将导致调用 add。:

var repoMock = new Mock<IRepository<Company>>();
repoMock.Setup(moq => moq.FindBy(It.IsAny<Expression<Func<Company, bool>>>())).Returns(new List<Company>());
var signupLogic = new SignupLogic(repoMock.Object);
signupLogic.AddNewCompany(new Company Name = "Company Inc");
repoMock.Verify(r => r.Add(It.IsAny<Company>()), Times.Once());

【讨论】:

仅在将另一个问题标记为答案后才看到此内容,但也会有所帮助。谢谢! It.IsAny 与不检查参数的Return() 结合使用的缺点是您无法验证参数是否正确。当然,有时您确实希望接受任何参数值,但在这种情况下,您可能需要验证 SignupLogic 是否正确构造了它的查询(使用公司名称,而不是其他属性)。 It.IsAny 总会通过。我更喜欢检查 lambda 表达式是否相等 m.FindBy(item=&gt;item.CompanyID == 1)。实现此目的的一种方法是使用 LambdaCompare:***.com/questions/283537/…。通过使用这个比较类,新设置将是Expression&lt;Func&lt;Company, bool&gt;&gt; testExpression = item =&gt; item.CompanyID == 1;repoMock.Setup(m =&gt; m.FindById(It.Is&lt;Expression&lt;Func&lt;Company, bool&gt;&gt;&gt;(criteria =&gt; LambdaCompare.Eq(criteria, testExpression)))).Returns(yourCompanyList)【参考方案3】:

您通常只模拟您拥有的类型。那些不属于你的,真的不应该因为各种困难而被嘲笑。因此,嘲弄表达 - 正如您问题的名称所暗示的那样 - 不是要走的路。

在起订量框架中。将.Returns() 用于函数很重要,否则不匹配。所以如果你还没有这样做,那是你的问题。

repoMock.Setup(moq => moq.FindBy(c => c.Name == "Company Inc").Returns(....

【讨论】:

正在嘲笑他拥有的一种类型 - RepositoryExpression 不是模拟的;它仅用于匹配模拟上的调用。 题目为Moq'ing Expression&lt;Func&lt;T, bool&gt;&gt; parameters 公平评论 Aliostad,标题有点不清楚,但我想问题的其余部分使我的意思清楚 - 毕竟这是一个相当详细的代码采样问题,即使我自己也这么说:) 即便如此,问题确实是“Moq'ing Expression parameters”,我试图在其中展示表达式正在传递的位置in 作为方法的参数。但是,我已经编辑了标题以使其更清晰:) 我尝试过这种方式,但是当我使用验证时它不起作用:结果消息:Moq.MockException:预期在模拟上调用一次,但为 0 次:u => u.Users.Any (a => a.Email == .user.Email) 配置设置:u => u.Users.Any(a => a.Email == .user.Email), Times.Never @Aliostad 您的答案很旧,但无法正常工作。 Moq 返回 UnsupportedException for Expression 的异常。表达式必须使用“It.Is/It.IsAny”传递

以上是关于将 Expression<Func<T, bool>> 作为参数传入的 Moq'ing 方法的主要内容,如果未能解决你的问题,请参考以下文章

将 List<Expression<Func<T, bool>>> 组合到 OR 子句 LINQ [重复]

从 Expression<Func<T, bool>> 转换为字符串

Expression<Func<T,TResult>>和Func<T,TResult>

Func<T> 如何隐式转换为 Expression<Func<T>>?

如何设置属性选择器的值 Expression<Func<T,TResult>>

Expression<Func<T,TResult>>和Func<T,TResult> 与AOP与WCF