使用匿名类型参数进行Moq验证

9
我有以下测试和相应的类,但我不知道如何验证对依赖项的调用。
[TestFixture]
public class AnonymousGenericTypeParameterTests
{
    [Test]
    public void Test()
    {
        // Arrange
        var dependency = new Mock<IDependency>();

        var towns = new List<Town>
        {
            new Town { Name = "Lifford", County = "Donegal", Country="Ireland", Population = 1658 },
            new Town { Name = "Ballyshannon", County = "Donegal", Country="Ireland", Population = 2504 },
            new Town { Name = "Buxton", County = "Derbyshire", Country="United Kingdom", Population = 13599 },
        };

        var sut = new MyClass(dependency.Object);

        // Act
        sut.DoSomething(towns);

        // Assert
        // The following line needs to be fixed.
        dependency.Verify(d => d.Execute(It.IsAny<IEnumerable<object>>(), It.IsAny<Func<object, decimal?>>()));
    }
}
public interface IDependency
{
    void Execute<T>(IEnumerable<T> collection, Func<T, decimal?> rateSelector);
}
public class MyClass
{
    private readonly IDependency dependency;
    public MyClass(IDependency dependency)
    {
        this.dependency = dependency;
    }
    public void DoSomething(IEnumerable<Town> towns)
    {
        var counties = towns.GroupBy(t => new {t.Country,t.County});
        foreach (var county in counties)
        {
            dependency.Execute(county, c => c.Population);
        }
    }
}
public class Town
{
    public string Name { get; set; }
    public string County { get; set; }
    public int Population { get; set; }
    public string Country { get; set; }
}

根据 Moq 的测试输出,执行的调用如下:
Dependency.Execute(System.Linq.Lookup`2+Grouping[<>f__AnonymousType0`2[System.String,System.String],UniFocus.Staffscope4.Tests.Town], System.Func`2[UniFocus.Staffscope4.Tests.Town,System.Nullable`1[System.Decimal]])

我看到有很多关于Moq中匿名参数的问题(例如这个这个这个),但找不到任何与使用匿名类型作为实际类型参数相关的内容。
在Verify行中可以放什么来验证调用内部?
注意:我的示例IDependency不返回值(已经足够复杂了),但回答中涉及到Setup()和Verify()将获得额外的赞誉。
更新 Jesse的解决方案之所以通过测试,是因为我在创建示例时做出了错误选择。我应该意识到任何IGrouping<out TKey, out TElement>也是一个IEnumerable<TElement>。是否有更通用的解决方案?
更新2 我觉得我的原始示例可能过于繁琐,没有很好地表达我的问题标题。是否有任何解决方案适用于这个更简单明了的示例?
using Moq;
using NUnit.Framework;

namespace Tests
{
    [TestFixture]
    public class SimpleAnonymousGenericTypeParameterTests
    {
        [Test]
        public void Test()
        {
            // Arrange
            var dependency = new Mock<IDependency>();
            var sut = new MyClass(dependency.Object);

            // Act
            sut.DoSomething("Donegal", "Lifford");

            // Assert
            // This verify works for both calls to Execute()
            dependency.Verify(d => d.Execute(It.IsAny<object>()), Times.Exactly(2));
            // This verify should specifically refer to only the first call to Execute()
            dependency.Verify(d => d.Execute(It.IsAny</*HowToRepresentAnonymousTypeHere*/object>()), Times.Once);
        }
        public interface IDependency
        {
            void Execute<T>(T thing);
        }
        public class MyClass
        {
            private readonly IDependency dependency;
            public MyClass(IDependency dependency)
            {
                this.dependency = dependency;
            }
            public void DoSomething(string county, string town)
            {
                dependency.Execute(new { county, town });
                object someUnknownObject = "";
                dependency.Execute(someUnknownObject);
            }
        }
    }
}
2个回答

10

对我来说,被接受的答案不起作用,我认为这是因为测试和相关对象在不同的程序集中,所以Moq不知道如何协调类型并使它们匹配。

相反,我创建了以下帮助方法,可以验证提供的匿名类型具有正确的字段和值:

public static class AnonHelpers
{
    public static object MatchAnonymousType(object expected)
    {
        return Match.Create(Matcher(expected));
    }

    private static Predicate<object> Matcher(object expected)
    {
        return actual =>
        {
            var expectedProp = expected.GetType().GetProperties().ToDictionary(x => x.Name, x => x.GetValue(expected));
            var actualProp = actual.GetType().GetProperties().ToDictionary(x => x.Name, x => x.GetValue(actual));

            foreach (var prop in expectedProp)
            {
                if (!actualProp.ContainsKey(prop.Key))
                    return false;
                if (!prop.Value.Equals(actualProp[prop.Key]))
                    return false;
            }
            return true;
        };
    }
}

它们可以这样使用:

var anon = new { SomeKey = "some value", SomeOtherKey = 123 };
myMock.Setup(x => x.MyMethod(personIDs, AnonHelpers.MatchAnonymousType(anon))).Verifiable();

这将创建一个匹配器,它将使用反射来基于键和值匹配匿名类型,然后您可以使用正常的验证来查看何时调用了它。


我猜如果匿名类型对象比较复杂,例如 new { prop = new { nested = 7 } },这个方法可能会失败。 此外,它似乎只检查了预期的属性...所以如果你的实际对象有额外的属性,它就不会失败? 我真的很喜欢这个方向,但需要填补一些漏洞,才能让它完全发挥作用。 - Lee Tickett

7

在测试上下文中已知类型,因此您可以向 Verify 调用提供特定的类型参数。以下更改使测试通过:

dependency.Verify(d =>
  d.Execute(It.IsAny<IEnumerable<Town>>(), It.IsAny<Func<Town, decimal?>>()));

同样的方法也适用于设置。

关于更新2中的示例,以下内容是正确的,但需要了解DoSomething()方法的内部工作原理,据我所知这是使其正常工作的唯一方法:

var anonymousType = new {county = "Donegal", town = "Lifford"};
dependency.Verify(d => d.Execute(anonymousType), Times.Once);

试图弄清楚这是如何通过的。Execute() 被调用如下:dependency.Execute(county, c => c.Population);。当我在那里悬停在 county 上时,我看到它的类型是 IGrouping<'a,Town>,其中 'a 是新的 { string Country, string County }。那么 Moq 是如何看到 Execute() 被传递了一个 IEnumerable<Town> 呢?然后我想到,一个 IGrouping<ANYTHING,Town> 也是一个 IEnumerable<Town>public interface IGrouping<out TKey, out TElement> : IEnumerable<TElement>, IEnumerable。所以你的编辑之所以通过,是因为我选择了一个不好的类型(IGrouping)作为示例 :( - mo.
DoSomething将IEnumerable<Town>分组,生成IEnumerable<Grouping<TKey,Town>>。然后,DoSomething对该IEnumerable<Grouping<TKey,Town>>进行迭代,并在每个IGrouping<TKey,Town>元素上调用IDependency.Execute<Town>。 TKey似乎并不重要 - 没有引用county.Key,因此匿名对象除了作为分组键之外从未发挥作用)。 IDependency.Execute<T>希望调用者提供T,在本例中始终是由DoSomething传递的Town。是否需要支持DoSomething的其他变体? - Jesse Sweetland
你给了我一个见解,解决了上面的示例,并解决了我在生产代码中遇到的原始问题,非常感谢!但是,我觉得这是对原始问题的一种变通方法,因为我的匿名类型(IGrouping<'a,Town>)实现了一个非匿名类型(IEnumerable<Town>),所以很容易将其与之验证。在我接受这个答案之前,你可以看一下我对问题的第二次更新,在那里我的示例似乎更准确地反映了问题的标题吗?我开始觉得在 Moq 中没有办法做到这一点。谢谢你的耐心! - mo.
1
以下代码可以通过,但需要了解DoSomething方法的内部工作原理,据我所知这是唯一使其正常工作的方法:var anonymousType = new {county = "Donegal", town = "Lifford"}; dependency.Verify(d => d.Execute(anonymousType), Times.Once); - Jesse Sweetland

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接