[英]Unit Testing Rich Domain Model
This was the anemic 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; }
}
And this is its behavior: 这是它的行为:
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;
}
}
And it's pretty much testable: 而且几乎可以测试:
[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>());
}
However, I wanted to practice Rich Domain Model, where data and behavior is more logically-cohesive. 但是,我想练习Rich Domain Model,其中数据和行为在逻辑上更加紧密。 So the code above is now converted to:
因此,上面的代码现在转换为:
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;
}
}
However I encounter testing problem: 但是我遇到测试问题:
[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>());
}
Unit test error: 单元测试错误:
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()
Seeing that directly invoking a method from the mocked's Object, the mocked object then can't detect if any of its property or method was called. 看到直接从模拟对象的Object调用方法后,模拟对象便无法检测其属性或方法是否被调用。 Having noticed that, what's the proper way to unit test a Rich Domain Model?
注意到这一点之后,对富域模型进行单元测试的正确方法是什么?
First, don't mock value objects or classes you are testing. 首先, 不要嘲笑您正在测试的值对象或类。 Also you are not verifying that correct modification date was provided to person.
另外,您没有验证是否已向人员提供了正确的修改日期。 You check that some date was assigned.
您检查是否已分配一些日期。 But that does not prove your code works as expected.
但这不能证明您的代码可以按预期工作。 In order to tests such code you should mock current date returned by DateTime.Now, or create some abstraction , which will provide current time to service.
为了测试这样的代码,您应该模拟 DateTime.Now返回的当前日期 ,或者创建一些抽象方法 ,以提供当前服务时间。 Your first test should look like (I used Fluent Assertions and NUnit here):
您的第一个测试应该看起来像(我在这里使用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();
}
Time provider is a simple abstraction: 时间提供者是一个简单的抽象:
public interface ITimeProvider
{
DateTime GetCurrentTime();
}
I'd go with singleton service instead of static class, because static classes are always problem - high coupling, no abstraction, hard to unit-test dependent classes. 我将使用单例服务而不是静态类,因为静态类总是存在问题-高耦合,没有抽象,难以对依赖类进行单元测试。 But you can inject time provider via property:
但是您可以通过属性注入时间提供者:
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();
}
}
Same relates to your second test. 同样与您的第二项测试有关。 Do not mock object you are testing.
不要嘲笑您正在测试的对象。 You should verify real code, which your application will use, instead of testing some mock, which is used only by test.
您应该验证应用程序将使用的真实代码,而不是测试仅由测试使用的模拟程序。 Test with reach domain model will look like:
使用到达域模型进行测试将如下所示:
[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();
}
Your call: 您的来电:
p.Object.UpdatePerson("John", "Lennon");
calls a public virtual
method UpdatePerson
on your mock. 在您的模拟上调用公共
virtual
方法UpdatePerson
。 Your mock has behavior Loose
(also known as Default
), and you did not Setup
that virtual method. 模拟对象的行为为
Loose
(也称为Default
),并且您没有Setup
该虚拟方法。
Moq's behavior in that case is to just do nothing in its implementation (override) of UpdatePerson
. 在这种情况下,Moq的行为是在
UpdatePerson
实现(重写)中什么也不做。
There are several ways you could change this. 您可以通过多种方式更改此设置。
virtual
keyword from UpdatePerson
method. UpdatePerson
方法中删除virtual
关键字。 Then Moq will not (and cannot) override its behavior. Setup
the virtual method with Moq before you call it. Setup
虚拟方法。 (Not useful in this case since it overrides the method you actually want to test.) Loose
behavior): If a virtual
member that was not setup is called, Moq will call the implementation of the base class. Loose
): 如果调用了未设置的virtual
成员,则Moq将调用基类的实现。 This explains what you saw. 这解释了您所看到的。 I can agree with the advice Sergey Berezovskiy gives in his answer.
我可以同意谢尔盖·贝雷佐夫斯基在回答中提出的建议。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.