Mocked method returns null when using anonymous types

风流意气都作罢 提交于 2020-01-25 02:57:25

问题


I have this code:

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

When I use this code, the company var is null. However, the commented out code works fine and results in a non null value.

Why does it not work with the anonymous type? Is there some way to get this to work with anonymous types?

NSubstitute version = 1.10.0.0.

.NET Framework version = 4.5.2.


回答1:


@Fabio's explanation is correct:

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.

You can read more about this in related questions such as NSubstitute - Testing for a specific linq expression.

Solving using a hand-coded substitute

Please see @Fabio's answer for a good explanation of how to hand-code a substitute to solve the problem and provide useful assertion messages. For complex substitutions sometimes it is simplest and most reliable to skip a library and to generate the exact type you need for your test.

Incomplete work-around with NSubstitute

This case in particular is more difficult than the standard expression-testing case (using Arg.Any or Arg.Is) because we can't explicitly refer to the anonymous type. We could use ReturnsForAnyArgs, but we need to be clear about which generic method version we're calling (again, we can't explicitly refer to the anonymous type required for T).

One hacky way to work around this is to pass the expression as you were originally doing (which gives us the correct generic type), and use ReturnsForAnyArgs so the exact identity of that expression does not matter.

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

As noted in @Nkosi's comment, this has the drawback that it only does a minimal assertion on the type used for the selector expression. This would also pass in the test above:

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

As an aside, we do get some very basic checking of the expression, as the combination of generic type and anonymous types means that selecting the wrong field will not compile. For example, if Company has string Name property we will get a compile error:

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)    
*/



回答2:


Because by default configured value will be returned only when arguments passed to the method are equal to the arguments configured with the mock.

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.

You can use working approach suggested by David and Dave.
Which solving compilation error when NuSubstitute can not figure out which type is used for a selector.

Such approaches will work, but for failing tests provides little information about actual reason (in case wrong selector is given to the method)

Sometimes implement your own mock will have some benefits

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

Usage

[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
}

And failed message now is more descriptive:
Expectation has member RegionId that the other object does not have.

Passing test

[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
}



回答3:


As Fabio and David Tchepak have already pointed out, my code wasn't working because it couldn't find a match for my method's argument because it was a different object to what was set up in the mock.

Here's another way to fix this:

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


来源:https://stackoverflow.com/questions/58927012/mocked-method-returns-null-when-using-anonymous-types

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!