public partial class Person
{
public virtual int PersonId { get; internal protected set; }
public virtual string Title { get; internal protected set; }
public virtual string FirstName { get; internal protected set; }
public virtual string MiddleName { get; internal protected set; }
public virtual string LastName { get; internal protected set; }
}
这就是它的行为:
public static class Services
{
public static void UpdatePerson(Person p, string firstName, string lastName)
{
// validate firstname and lastname
// if there's a curse word, throw an exception
// if valid, continue
p.FirstName = firstName;
p.LastName = lastName;
p.ModifiedDate = DateTime.Now;
}
}
这几乎是可以测试的:
[TestMethod]
public void Is_Person_ModifiedDate_If_Updated()
{
// Arrange
var p = new Mock<Person>();
// Act
Services.UpdatePerson(p.Object, "John", "Lennon");
// Assert
p.VerifySet(x => x.ModifiedDate = It.IsAny<DateTime>());
}
但是,我想练习Rich Domain Model,其中数据和行为更具逻辑连贯性。所以上面的代码现在转换为:
public partial class Person
{
public virtual int PersonId { get; internal protected set; }
public virtual string Title { get; internal protected set; }
public virtual string FirstName { get; internal protected set; }
public virtual string MiddleName { get; internal protected set; }
public virtual string LastName { get; internal protected set; }
public virtual void UpdatePerson(string firstName, string lastName)
{
// validate firstname and lastname
// if there's a curse word, throw an exception
// if valid, continue
this.FirstName = firstName;
this.LastName = lastName;
this.ModifiedDate = DateTime.Now;
}
}
但是我遇到了测试问题:
[TestMethod]
public void Is_Person_ModifiedDate_If_Updated()
{
// Arrange
var p = new Mock<Person>();
// Act
p.Object.UpdatePerson("John", "Lennon");
// Assert
p.VerifySet(x => x.ModifiedDate = It.IsAny<DateTime>());
}
单元测试错误:
Result Message:
Test method Is_Person_ModifiedDate_If_Updated threw exception:
Moq.MockException:
Expected invocation on the mock at least once, but was never performed: x => x.ModifiedDate = It.IsAny<DateTime>()
No setups configured.
Performed invocations:
Person.UpdatePerson("John", "Lennon")
Result StackTrace:
at Moq.Mock.ThrowVerifyException(MethodCall expected, IEnumerable`1 setups, IEnumerable`1 actualCalls, Expression expression, Times times, Int32 callCount)
at Moq.Mock.VerifyCalls(Interceptor targetInterceptor, MethodCall expected, Expression expression, Times times)
at Moq.Mock.VerifySet[T](Mock`1 mock, Action`1 setterExpression, Times times, String failMessage)
at Moq.Mock`1.VerifySet(Action`1 setterExpression)
at Is_Person_ModifiedDate_If_Updated()
看到直接从模拟的Object调用方法,模拟对象无法检测是否调用了任何属性或方法。注意到,对Rich Domain Model进行单元测试的正确方法是什么?
答案 0 :(得分:5)
首先,don't mock value objects或您正在测试的课程。此外,您无法验证是否向人员提供了正确的修改日期。您检查是否分配了某个日期。但这并不能证明您的代码按预期工作。为了测试这样的代码,您应该{Date} .Now或mock current date返回create some abstraction,这将提供当前的服务时间。你的第一个测试应该是这样的(我在这里使用了Fluent Assertions和NUnit):
[Test]
public void Should_Update_Person_When_Name_Is_Correct()
{
// Arrange
var p = new Person(); // person is a real class
var timeProviderMock = new Mock<ITimeProvider>();
var time = DateTime.Now;
timeProviderMock.Setup(tp => tp.GetCurrentTime()).Returns(time);
Services.TimeProvider = timeProviderMock.Object;
// Act
Services.UpdatePerson(p, "John", "Lennon");
// Assert
p.FirstName.Should().Be("John");
p.LastName.Should().Be("Lennon");
p.ModifiedDate.Should().Be(time); // verify that correct date was set
timeProviderMock.VerifyAll();
}
时间提供者是一个简单的抽象:
public interface ITimeProvider
{
DateTime GetCurrentTime();
}
我会使用单例服务而不是静态类,因为静态类总是存在问题 - 高耦合,无抽象,难以单元测试依赖类。但是你可以通过属性注入时间提供者:
public static class Services
{
public static ITimeProvider TimeProvider { get; set; }
public static void UpdatePerson(Person p, string firstName, string lastName)
{
p.FirstName = firstName;
p.LastName = lastName;
p.ModifiedDate = TimeProvider.GetCurrentTime();
}
}
同样与你的第二次测试有关。不要模拟你正在测试的对象。您应该验证应用程序将使用的实际代码,而不是测试一些仅由测试使用的mock。使用覆盖域模型进行测试将如下所示:
[Test]
public void Should_Update_Person_When_Name_Is_Correct()
{
// Arrange
var timeProviderMock = new Mock<ITimeProvider>();
var time = DateTime.Now;
timeProviderMock.Setup(tp => tp.GetCurrentTime()).Returns(time);
var p = new Person(timeProviderMock.Object); // person is a real class
// Act
p.Update("John", "Lennon");
// Assert
p.FirstName.Should().Be("John");
p.LastName.Should().Be("Lennon");
p.ModifiedDate.Should().Be(time); // verify that correct date was set
timeProviderMock.VerifyAll();
}
答案 1 :(得分:1)
您的电话:
p.Object.UpdatePerson("John", "Lennon");
在您的模拟上调用公开的 virtual
方法UpdatePerson
。您的模拟具有行为Loose
(也称为Default
),并且您没有Setup
该虚拟方法。
Moq在这种情况下的行为是在UpdatePerson
的实现(覆盖)中只执行 nothing 。
有几种方法可以改变它。
virtual
方法中删除UpdatePerson
关键字。然后Moq不会(也不能)覆盖它的行为。Setup
虚拟方法。 (在这种情况下没用,因为它会覆盖你实际想要测试的方法。)Loose
行为):如果调用了未设置的virtual
成员,Moq将调用基类的实现。这解释了你所看到的。我同意谢尔盖·别列佐夫斯基在答案中给出的建议。