需要帮助更好地理解Moq

50

我一直在查看Moq文档,但注释对我来说太短了,无法理解它所能做的每一个事情。

首先,我不明白的是 It.IsAny<string>(). //example using string 是什么意思。

使用这个有什么优势,而不是只放一些值进去?我知道人们说如果你不关心值就使用这个,但是如果你不关心值,你不能只写"a"或其他吗?这似乎只是更多的输入。

其次,什么情况下你会不关心值?我认为Moq需要值以匹配东西。

我完全不理解 It.Is<> 是什么或如何使用它。我不理解这个示例以及它试图展示什么。

接下来,我不知道什么时候使用 Times(以及它的 AtMost 方法和类似方法)。为什么要限制某些设置的次数?我有一些需要使用两次的 AppConfig 值。为什么我想限制它只有一次呢?这将使测试失败。这是阻止其他人向您的代码添加其他设置的方式吗?

我不知道如何使用 mock.SetupAllProperties(); 它设置属性为什么?

我也不明白为什么有这么多不同的方法来设置属性,它们之间的区别是什么。文档中提到了:

SetupGet(of property)
SetupGet<TProperty>

我注意到Moq中有很多内容显示为()<>-它们之间有什么区别,使用时会是什么样子?

我也不明白为什么他们有SetupGet。难道不应该使用SetupSet来设置属性吗?文档中的SetupSet有五种不同的用法。另外还有一个叫做SetupProperty的方法。所以我不明白为什么有这么多。

另外,我想知道在lambda表达式中使用的变量是否独立于其他lambda表达式。例如:

mock.setup(m => m.Test);
stop.setup(m => m.Test);

这样做可以吗?还是变量m之间会有冲突?

最后,我看了这个视频,想知道它是否显示的是Visual Studio。他的Intellisense看起来有些不同。一个灯泡弹出来(我很高兴我的没有,因为它让我想起了NetBeans的痛苦回忆),还有从左括号到右括号等的连线。

2个回答

116

It.IsAny / It.Is

在测试代码中传递新的引用类型时,这些方法非常有用。例如,如果你有一个类似以下代码的方法:

public void CreatePerson(string name, int age) {
    Person person = new Person(name, age);
    _personRepository.Add(person);
}

你可能想要检查仓库(repository)上是否已调用了 add 方法。

[Test]
public void Create_Person_Calls_Add_On_Repository () {
    Mock<IPersonRepository> mockRepository = new Mock<IPersonRepository>();
    PersonManager manager = new PersonManager(mockRepository.Object);
    manager.CreatePerson("Bob", 12);
    mockRepository.Verify(p => p.Add(It.IsAny<Person>()));
}

如果您想让此测试更加明确,可以使用It.Is方法并提供一个必须匹配 person 对象的谓词。

[Test]
public void Create_Person_Calls_Add_On_Repository () {
    Mock<IPersonRepository> mockRepository = new Mock<IPersonRepository>();
    PersonManager manager = new PersonManager(mockRepository.Object);
    manager.CreatePerson("Bob", 12);
    mockRepository.Verify(pr => pr.Add(It.Is<Person>(p => p.Age == 12)));
}

如果使用的person对象在调用add方法时没有将年龄属性设置为12,这样测试将会抛出一个异常。

时间

如果你有一个类似以下方法的方法:

public void PayPensionContribution(Person person) {
    if (person.Age > 65 || person.Age < 18) return;
    //Do some complex logic
    _pensionService.Pay(500M);
}

你可能想要测试的一件事情是,当一个年龄超过65岁的人被传递到"pay"方法时,该方法不会被调用。

[Test]
public void Someone_over_65_does_not_pay_a_pension_contribution() {
    Mock<IPensionService> mockPensionService = new Mock<IPensionService>();
    Person p = new Person("test", 66);
    PensionCalculator calc = new PensionCalculator(mockPensionService.Object);
    calc.PayPensionContribution(p);
    mockPensionService.Verify(ps => ps.Pay(It.IsAny<decimal>()), Times.Never());
}

同样地,你可以想象在遍历集合并对集合中的每个项目调用一个方法的情况下,你希望确保它已被调用了一定次数,而有时你则不关心。

SetupGet / SetupSet

需要注意的是,这些东西反映了你的代码如何与模拟对象交互,而不是你如何设置模拟对象。

public static void SetAuditProperties(IAuditable auditable) {
    auditable.ModifiedBy = Thread.CurrentPrincipal.Identity.Name;
}
在这种情况下,代码设置了实现 IAuditable 接口的对象的 ModifiedBy 属性,同时获取了实现 IPrincipal 接口的当前对象的 Name 属性。
[Test]
public void Accesses_Name_Of_Current_Principal_When_Setting_ModifiedBy() {
    Mock<IPrincipal> mockPrincipal = new Mock<IPrincipal>();
    Mock<IAuditable> mockAuditable = new Mock<IAuditable>();

    mockPrincipal.SetupGet(p => p.Identity.Name).Returns("test");

    Thread.CurrentPrincipal = mockPrincipal.Object;
    AuditManager.SetAuditProperties(mockAuditable.Object);

    mockPrincipal.VerifyGet(p => p.Identity.Name);
    mockAuditable.VerifySet(a => a.ModifiedBy = "test");
}
在这种情况下,我们正在为IPrincipal的模拟设置名称属性,因此当在Identity上调用Name属性的getter时,它将返回“test”,而不是设置属性本身。

SetupProperty / SetupAllProperties

如果将上面的测试更改为以下内容:

[Test]
public void Accesses_Name_Of_Current_Principal_When_Setting_ModifiedBy() {
    Mock<IPrincipal> mockPrincipal = new Mock<IPrincipal>();
    Mock<IAuditable> mockAuditable = new Mock<IAuditable>();
    mockPrincipal.SetupGet(p => p.Identity.Name).Returns("test");

    var auditable = mockAuditable.Object;

    Thread.CurrentPrincipal = mockPrincipal.Object;
    AuditManager.SetAuditProperties(auditable);

    Assert.AreEqual("test", auditable.ModifiedBy);
}

测试将会失败。这是因为 Moq 创建的代理在属性的 set 方法中实际上不会执行任何操作,除非你明确指定它执行。实际上,模拟对象看起来有点像这样

public class AuditableMock : IAuditable {
     public string ModifiedBy { get { return null; } set { } }

} 

为了使测试通过,您需要告诉Moq设置属性以具有标准的属性行为。您可以通过调用SetupProperty来实现这一点,mock将看起来更像:

public class AuditableMock : IAuditable {
     public string ModifiedBy { get; set; }
} 

现在,由于模拟对象存储了"value"为"test",因此上述测试将通过。当模拟复杂对象时,您可能希望为所有属性执行此操作,因此使用SetupAllProperties快捷方式。

最后,IDE中的灯泡是ReSharper插件。


你可能想要检查仓库上是否已调用了add方法 - 呃,为什么你要检查这个?它就在代码里面啊。这不是一个典型的冗余测试吗?它会给出错误的信心因为它测试了已经被建立的不变量,从而导致选择偏差。 - Konrad Rudolph
5
这是一个过度规定代码行为的测试,但同时也是一个很简洁地演示了It.IsAny的测试,而这正是原问题所涉及的。 - John Foster
5
Konrad,添加这样的测试的整个目的是为了发现回归性问题。如果有人进去修改CreatePerson方法并更改其实现,以便不明确调用“Add”,你需要知道它。因此,通过有一个确认方法被调用的测试,就没有任何歧义了。请记住,在一个大而复杂的代码库中,说“它就在代码里”是不相关的。所有东西都在代码中 - 测试的存在是为了确保代码中所存在的就是应该存在的。 - Webreaper

6

如果您不关心属性的确切值,使用 .IsAny 要好得多,因为这样可以明确表达确切值并不重要。如果将其硬编码为 "abc",则无法确定您正在测试的代码是否依赖于以 "a" 开头、以 "c" 结尾或长度为 3 等等。


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