单元测试富域模型

Hao*_*Hao 2 c# unit-testing moq 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; } 
}
Run Code Online (Sandbox Code Playgroud)

这是它的行为:

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;
    }

}
Run Code Online (Sandbox Code Playgroud)

而且几乎可以测试:

[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>());
}
Run Code Online (Sandbox Code Playgroud)

但是,我想练习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;
    }           
}
Run Code Online (Sandbox Code Playgroud)

但是我遇到测试问题:

[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>());
}
Run Code Online (Sandbox Code Playgroud)

单元测试错误:

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()
Run Code Online (Sandbox Code Playgroud)

看到直接从模拟对象的Object调用方法,然后模拟对象就无法检测是否调用了其任何属性或方法。注意到这一点之后,对富域模型进行单元测试的正确方法是什么?

Ser*_*kiy 5

首先,不要嘲笑您正在测试的值对象或类。另外,您没有验证是否已向人员提供了正确的修改日期。您检查是否已分配一些日期。但这不能证明您的代码可以按预期工作。为了测试这样的代码,您应该模拟 DateTime.Now返回的当前日期,或者创建一些abstract,它将提供当前服务时间。您的第一个测试应该看起来像(我在这里使用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();
}
Run Code Online (Sandbox Code Playgroud)

时间提供者是一个简单的抽象:

public interface ITimeProvider
{
    DateTime GetCurrentTime();
}
Run Code Online (Sandbox Code Playgroud)

我将使用单例服务而不是静态类,因为静态类总是存在问题-高耦合,没有抽象,难以对依赖类进行单元测试。但是您可以通过属性注入时间提供者:

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();
    }
}
Run Code Online (Sandbox Code Playgroud)

同样与您的第二项测试有关。不要嘲笑您正在测试的对象。您应该验证应用程序将使用的真实代码,而不是测试仅由测试使用的模拟程序。使用到达域模型进行测试将如下所示:

[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();
}
Run Code Online (Sandbox Code Playgroud)