Moq 是否模拟子接口返回值并忽略中间步骤是错误或功能

本文关键字:中间 错误 功能 模拟 是否 接口 返回值 Moq | 更新日期: 2023-09-27 18:35:43

我最近正在构建一个应用程序,一位同事写了一个我发誓会失败的设置。我错了。在其中设置了一个工厂方法,其预期值为 true,并将返回一个整数。因为我们没有模拟我们的配置,所以布尔值总是假的。

设置是:

 var homeStoreDataServiceFactory = new Mock<IHomeStoreDataServiceFactory>();
 homeStoreDataServiceFactory.Setup(service => service.Create(true).GetStoreNumber())
                .Returns(5);

我认为对 factory.Create(false) 的调用不会生成模拟对象,因此我们将得到整数的 0 而不是模拟值 5。相反,无论我们改变了什么服务。Create(X) to,对 GetStoreNumber 的调用总是返回 5,就好像我们使用了 It.IsAny() 一样。

我已经建立了一个MVCE,以便您可以看到我的困惑:

using System;
using Moq;
namespace MoqBugMCV
{
    public interface IItemServiceFactory
    {
        IItemService Create(bool shouldCreateServiceA);
    }
    public class Item
    {
        public string Name { get; set; }
        public decimal Price { get; set; }
    }
    public interface IItemService
    {
        Item GetItem();
    }
    public class ItemManager
    {
        private readonly IItemService _itemService;
        public ItemManager(IItemServiceFactory itemServiceFactory)
        {
            _itemService = itemServiceFactory.Create(true); //<==== configured true (like by app.config at runtime or something)
        }
        public Item GetAnItem()
        {
            return _itemService.GetItem();
        }
    }
    internal class Program
    {
        private static void Main(string[] args)
        {
            var itemServiceFactory = new Mock<IItemServiceFactory>();
            var chrisItem = new Item {Name = "Chris's Amazing Item", Price = 1000000};
            itemServiceFactory.Setup(factory => factory.Create(true).GetItem())
                .Returns(chrisItem);
            var itemManager = new ItemManager(itemServiceFactory.Object);
            var theItem = itemManager.GetAnItem();
            Console.WriteLine("The item is {0} and costs {1}", theItem.Name, theItem.Price);
            var itemServiceFactoryBroken = new Mock<IItemServiceFactory>();
            itemServiceFactoryBroken.Setup(factory => factory.Create(false).GetItem()).Returns(chrisItem); //expecting this to fail, because IItemServiceFactory.Create(true) is configured
            itemManager = new ItemManager(itemServiceFactoryBroken.Object);
            theItem = itemManager.GetAnItem();
            Console.WriteLine("The item is {0} and costs {1}", theItem.Name, theItem.Price); //would expect the item would be null or values to be blank
        }
    }
}

那么......这是一个错误或功能,还是我对Moq不了解?

Moq 是否模拟子接口返回值并忽略中间步骤是错误或功能

是的,似乎根据此处的说法,应用于递归 Moq 上的中间取消引用的任何过滤器都被忽略了 - 即使使用It.Is<>匹配器,过滤器也会被忽略。 即,正如您所指出的,目前递归模拟将始终在中间取消引用的任何参数上生成等效的It.IsAny<>

现在,我会分手:

.Setup(service => service.Create(true).GetStoreNumber()

并将其拆分为:

mockFactory.Setup(f => f.Create(xx))
mockService.Setup(service => service.GetStoreNumber())

将此分离应用于您的MVCE示例,下面的设置仅模拟factory.Create(false),将引用类型的默认返回值factory.Create(true) null

var itemServiceFactory = new Mock<IItemServiceFactory>();
var itemService = new Mock<IItemService>();
itemService.Setup(svc => svc.GetItem()).Returns(chrisItem);
itemServiceFactory.Setup(factory => factory.Create(false))
    .Returns(itemService.Object);
var itemManager = new ItemManager(itemServiceFactory.Object);
var theItem = itemManager.GetAnItem(); // Get NRE on _itemService.GetItem as expected

因此,模拟factory.Create(true)将使用设置服务,GetAnItem()将返回惊人的项目。

编辑

似乎有一种方法可以在中间步骤中实现过滤器,并使用 Linq to Mocks 一次性压缩工厂和服务模拟的设置:

var itemServiceFactory = Mock.Of<IItemServiceFactory>(
    fac => fac.Create(false) == Mock.Of<IItemService>(
        svc => svc.GetItem() == chrisItem));
var itemManager = new ItemManager(itemServiceFactory);
var theItem = itemManager.GetAnItem();

在对其他一些事情进行了一些角力之后,我回到了这个进一步调查。

如果将第二个示例更改为:

        var itemServiceFactoryBroken = new Mock<IItemServiceFactory>(MockBehavior.Strict); //Strict behavior is important!!
        itemServiceFactoryBroken.Setup(factory => factory.Create(false).GetItem()).Returns(chrisItem); //expecting this to fail, because IItemServiceFactory.Create(true) is configured
        itemManager = new ItemManager(itemServiceFactoryBroken.Object);
        theItem = itemManager.GetAnItem();
        Console.WriteLine("The item is {0} and costs {1}", theItem.Name, theItem.Price);

它将引发异常,因为未配置安装程序。我仍然相信这是一个错误,它绝对不是你期望发生的事情。GetItem 不应该返回 chrisItem,因为工厂没有设置为输出一个模拟类,该类返回给定所测试系统配置的任何内容。

但是,严格行为模拟似乎"知道"此设置无效,并将引发异常。在这些情况下,行为模式似乎存在鸿沟。