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;
_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插件。