Unit Testing Rich Domain Model

2019-07-17 02:51发布

问题:

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. 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. Having noticed that, what's the proper way to unit test a Rich Domain Model?

回答1:

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. Your first test should look like (I used Fluent Assertions and NUnit here):

[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();
}


回答2:

Your call:

p.Object.UpdatePerson("John", "Lennon");

calls a public virtual method UpdatePerson on your mock. Your mock has behavior Loose (also known as Default), and you did not Setup that virtual method.

Moq's behavior in that case is to just do nothing in its implementation (override) of UpdatePerson.

There are several ways you could change this.

  • You could remove virtual keyword from UpdatePerson method. Then Moq will not (and cannot) override its behavior.
  • Or you could actually Setup the virtual method with Moq before you call it. (Not useful in this case since it overrides the method you actually want to test.)
  • Or you could say 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.

This explains what you saw. I can agree with the advice Sergey Berezovskiy gives in his answer.