使用匿名类型时模拟方法 returns null

Mocked method returns null when using anonymous types

我有这个代码:

using NSubstitute;
using NUnit.Framework;
using System;
using System.Linq.Expressions;

namespace MyTests
{
    public interface ICompanyBL
    {
        T GetCompany<T>(Expression<Func<Company, T>> selector);
    }

    public partial class Company
    {
        public int RegionID { get; set; }
    }

    public class Tests
    {
        [Test]
        public void Test()
        {
            var companyBL = Substitute.For<ICompanyBL>();

            //Doesn't work
            companyBL.GetCompany(c => new { c.RegionID }).Returns(new
            {
                RegionID = 4,
            });

            //Results in null:
            var company = companyBL.GetCompany(c => new { c.RegionID });

            //This works:
            //companyBL.GetCompany(Arg.Any<Expression<Func<Company, Company>>>()).Returns(new Company
            //{
            //    RegionID = 4,
            //});

            //Results in non null:
            //var company = companyBL.GetCompany(c => new Company { RegionID = c.RegionID });
        }
    }
}

当我使用这段代码时,company 变量为空。 但是,注释掉的代码工作正常并产生非空值。

为什么它不适用于匿名类型? 有什么方法可以让它与匿名类型一起使用吗?

N替代版本=1.10.0.0.

.NET 框架版本 = 4.5.2.

因为默认情况下,只有当传递给方法的参数等于使用 mock 配置的参数时,才会返回配置的值。

Expression<Func<Company, T>> 是引用类型,当两个实例都引用同一个对象时,将等于另一个实例。

在您的情况下,配置的模拟代码和实际代码接收两个不同对象的不同实例。

您可以使用 David 和 Dave 建议的工作方法。
当 NuSubstitute 无法确定选择器使用哪种类型时,解决了编译错误。

这样的方法会起作用,但对于失败的测试,提供的实际原因信息很少(以防为方法提供了错误的选择器)

有时实现自己的 mock 会有一些好处

public class FakeBusiness : ICompanyBL
{
    private MyCompany _company;

    public FakeBusiness For(MyCompany company)
    {
        _company = company;
        return this;
    }

    public T GetCompany<T>(Expression<Func<MyCompany, T>> selector)
    {
        return selector.Compile().Invoke(_company);
    }
}

用法

[Fact]
public void TestObjectSelector()
{
    var company = new MyCompany { RegionId = 1, Name = "One" };
    var fakeBl = new FakeBusiness().For(company); // Configure mock

    var actual = fakeBl.GetCompany(c => new { c.Name }); // Wrong selector

    actual.Should().BeEquivalentTo(new { RegionId = 1 }); //Fail
}

现在失败的消息更具描述性:
期望有另一个对象没有的成员RegionId。

通过测试

[Fact]
public void TestObjectSelector()
{
    var company = new MyCompany {RegionId = 1, Name = "One"};
    var fakeBl = new FakeBusiness().For(company); // Configure mock

    var actual = fakeBl.GetCompany(c => new { c.RegionId });

    actual.Should().BeEquivalentTo(new { RegionId = 1 }); // Ok
}

正确:

Expression<Func<Company, T>> is a reference type and will be equal to another instance when both instances reference same object.

In your case configured mock and actual code receive different instances of two different objects.

您可以在 NSubstitute - Testing for a specific linq expression.

等相关问题中阅读更多相关信息

使用手写替代求解

请参阅 以了解如何手动编写替代代码来解决问题并提供有用的断言消息的详细说明。对于复杂的替换,有时跳过库并生成测试所需的确切类型是最简单和最可靠的。

使用 NSubstitute 的不完整解决方法

这种情况尤其比标准表达式测试情况(使用 Arg.AnyArg.Is)更难,因为我们不能显式引用匿名类型。我们可以使用 ReturnsForAnyArgs,但我们需要清楚我们正在调用哪个泛型方法版本(同样,我们不能显式引用 T 所需的匿名类型)。

解决此问题的一种 hacky 方法是像您最初所做的那样传递表达式(这为我们提供了正确的泛型类型),并使用 ReturnsForAnyArgs 因此该表达式的确切标识无关紧要。

[Fact]
public void Test() {
    var companyBL = Substitute.For<ICompanyBL>();

    // Keep expression in `GetCompany` so it calls the correct generic overload.
    // Use `ReturnsForAnyArgs` so the identity of that expression does not matter.
    companyBL.GetCompany(c => new { c.RegionID }).ReturnsForAnyArgs(new {
        RegionID = 4,
    });

    var company = companyBL.GetCompany(c => new { c.RegionID });

    Assert.NotNull(company);
}

如@Nkosi 的评论所述,它的缺点是它只对用于选择器表达式的类型进行最小断言。这也将通过上面的测试:

var company = companyBL.GetCompany(c => new { RegionID = 123 });

顺便说一句,我们确实对表达式进行了一些非常基本的检查,因为泛型类型和匿名类型的组合意味着选择错误的字段将无法编译。例如,如果 Companystring Name 属性 我们将得到一个编译错误:

companyBL.GetCompany(c => new { c.RegionID }).ReturnsForAnyArgs(new { RegionID = 4 });

var company= companyBL.GetCompany(c => new { c.Name });
Assert.Equal(4, company.RegionID); // <- compile error CS1061

/* 
Error CS1061: '<anonymous type: string Name>' does not contain a definition 
for 'RegionID' and no accessible extension method 'RegionID' accepting a first 
argument of type '<anonymous type: string Name>' could be found (are you missing
a using directive or an assembly reference?) (CS1061)    
*/

正如 Fabio 和 David Tchepak 已经指出的那样,我的代码无法正常工作,因为它找不到与我的方法参数匹配的对象,因为它与模拟中设置的对象不同。

这是解决此问题的另一种方法:

    [Test]
    public void Test()
    {
        var companyBL = Substitute.For<ICompanyBL>();
        Expression<Func<Company, object>> x = c => new { c.RegionID };
        companyBL.GetCompany(x).Returns(new
        {
            RegionID = 4,
        });

        var company = companyBL.GetCompany(x);
    }

假设之前的选项可能不是您想要的,如果您可以访问 CompanyBL 的(在本例中)源代码,则您可以使用另一种方法。

此替代方法是将需要模拟的方法标记为 virtual 并让模拟继承具体 class CompanyBL 而不是实现整个接口。

您可能想要这样做的原因是让模拟不那么脆弱..即当有人扩展 ICompanyBL 接口时,他们不需要向模拟添加实现。

虽然这不是具体如何使用 NSubstitute 的答案,但我认为它是一个有价值的选择。一种变体可能是将 CompanyBL 更改为具有受保护的虚拟方法,以实现需要在模拟中替换的功能。编写更可测试的代码应该是一个值得关注的问题,可以说值得进行这些类型的更改。