简体   繁体   English

单元测试富域模型

[英]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. 您可以通过多种方式更改此设置。

  • You could remove virtual keyword from UpdatePerson method. 您可以从UpdatePerson方法中删除virtual关键字。 Then Moq will not (and cannot) override its behavior. 然后Moq将不会(也不能)覆盖其行为。
  • Or you could actually Setup the virtual method with Moq before you call it. 或者,您实际上可以在调用Moq之前Setup虚拟方法。 (Not useful in this case since it overrides the method you actually want to test.) (在这种情况下没有用,因为它会覆盖您实际要测试的方法。)
  • Or you could say p.CallBase = true; 或者你可以说p.CallBase = true; before you call the method. 在调用该方法之前。 This works as follows (with 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.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM