用NSubstitute模拟表达式

本文关键字:表达式 模拟 NSubstitute | 更新日期: 2023-09-27 18:25:08

我有一个包含以下方法签名的接口:

TResult GetValue<T, TResult>(object key, Expression<Func<T, TResult>> property) where T : class;

使用Moq,我可以模拟这种方法的特定调用,如下所示:

var repo = new Mock<IRepository>();
repo.Setup(r => r.GetValue<Customer, string>("SomeCustomerId", c => c.SecretAgentId)).Returns("SecretAgentId");

然后当我这样做的时候,打电话给

repo.Object.GetValue<Customer, string>("SomeCustomerId", c => c.SecretAgentId);

正如我所期望的那样,Tt返回"SecretAgentId",所以一切看起来都很好。

我的问题是,在我们真正的生产代码中,我们使用的是NSubstitute,而不是Moq。我尝试在这里使用相同类型的设置:

var repo = Substitute.For<ICrmRepository>();
repo.GetValue<Customer, string>("SomeCustomerId", c => c.SecretAgentId).Returns("SecretAgentId");

然而,当我在这里打以下电话时,

repo.GetValue<Customer, string>("SomeCustomerId", c => c.SecretAgentId);

它返回"而不是"SecretAgentId"

我试着用Arg.Any<Expression<Func<Customer, string>>>()替换c => c.SecretAgentId,只是想看看它当时是否有效,然后它按预期返回"SecretAgentId"。但我需要验证它是用正确的表达式调用的,而不仅仅是任何表达式。

所以我需要知道是否有可能在NSubstitute中实现这一点,如果有,如何实现?

用NSubstitute模拟表达式

我认为表达式在NSubstitute中的求值取决于它们的闭包范围,所以这两个表达式声明是不相同的。在我看来,这就像一个bug,你可能想打开一个问题。

然而,您可以将表达式从替换声明中删除,并且它可以正确工作:

private static void Main(string[] args)
{
    Expression<Func<string, string>> myExpression =  s => s.Length.ToString();
    var c = Substitute.For<IRepo>();
    c.GetValue<string, string>("c", myExpression).Returns("C");
    var result = c.GetValue<string, string>("c", myExpression); // outputs "C"
}

我记不清确切的语法,所以如果这不是A1正确的,请原谅我,它有点笨拙,但。。。

我相信当你尝试Arg时,你是在正确的轨道上。然而,尝试使用Arg。是这样的:

Arg.Is<Expression<Func<Customer, string>>>(x => { 
    var m = ((Expression)x).Body as MemberExpression;
    var p = m.Member as PropertyInfo;
    return p.Name == "SecretAgentId";
});

正如@Fordio已经指出的那样,Arg.Is<T>()是我们要走的路。您可以使用它来指定参数必须满足的条件,以便提供预期的结果。当您无法控制参数的初始化并且无法提供对Returns()的正确引用时,它尤其有用,该引用对以后的方法调用有效。

例如,您想模拟以下接口:

 public interface IQueryCache
 {
     TResult GetOrExecute<TQuery, TResult>(TQuery query, Func<TResult> func)
         where TQuery : IQuery<TResult>
         where TResult : class;
 }

它被另一个自己初始化GetOrExecute()参数的类使用:

public class RequestHandler
{
    private readonly IQueryCache _cache;
    private readonly IRepository _repository;
    public RequestHandler(IRepository repository, IQueryCache cache)
    {
        _repository = repository;
        _cache = cache;
    }
    public TResult GetResult(Guid userId)
    {
        var query = new Query() { UserId = userId };
        var result = _cache.GetOrExecute(query, () => _repository.GetUserItems(query));
        return result;
    }
}

因此,如果您想模拟缓存并只测试RequestHandler,以下设置将不起作用,因为您无法提供对参数的引用,该参数在GetRestult():中初始化

var repository = Substitute.For<IRepository>();
var cache = Substitute.For<IQueryCache>();
repository.GetUserItems(query).Returns(expected);
cache.GetOrExecute(query, () => repository.GetUserItems(query)).Returns(expected);
var handler = new RequestHandler(repository, cache);

然而,你可以通过在参数的属性上指定条件来解决这个问题,比如:

cache.GetOrExecute(Arg.Is<Query>(q => q.UserId == "value"), Arg.Any<Func<IEnumerable<TResult>>>()).Returns(expected);