问题
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