【问题标题】:Unit Testing Rich Domain Model单元测试富域模型
【发布时间】:2013-12-31 05:23:48
【问题描述】:

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

这是它的行为:

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

但是,我想练习富域模型,其中数据和行为在逻辑上更具凝聚力。所以上面的代码现在转换为:

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()

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

【问题讨论】:

  • 感谢您取消删除,如下所示,写答案花了一些时间:)
  • 更新方法,虽然如您所展示的那样有利于为验证目的进行封装,但实际上只是渗入域的事务性脚本。尝试打破用户的要求。他们是在更正此人姓名的拼写,还是更改此人的法定姓名。当然,这取决于您的应用程序,但请尝试考虑细粒度的请求以及执行时产生的行为。

标签: c# unit-testing moq rich-domain-model


【解决方案1】:

首先,don't mock value objects 或您正在测试的课程。此外,您没有验证是否向人员提供了正确的修改日期。您检查是否分配了某个日期。但这并不能证明您的代码按预期工作。为了测试此类代码,您应该 mock current date 由 DateTime.Now 返回,或 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();
    }
}

第二次测试也是如此。不要模拟您正在测试的对象。您应该验证您的应用程序将使用的真实代码,而不是测试一些仅用于测试的模拟。使用到达域模型进行测试如下所示:

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

【讨论】:

  • 看起来是一个不错的解决方案,我应该重新考虑何时模拟或不模拟。也许我应该更多地依赖于测试实际对象,并且仅在测试实际对象很愚蠢的情况下使用模拟对象,例如下载文件并等待它完成的组件
  • 我会考虑在对 Update 的调用中传递日期,这样您就可以避免存根和需要依赖“时钟”的人的奇怪。
【解决方案2】:

您的电话:

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

在您的模拟上调用公共 virtual 方法 UpdatePerson。您的 mock 具有行为 Loose(也称为 Default),而您没有 Setup 那个虚拟方法。

在这种情况下,Moq 的行为是在其实现(覆盖)UpdatePerson什么都不做

有几种方法可以改变这一点。

  • 您可以从UpdatePerson 方法中删除virtual 关键字。那么 Moq 将不会(也不能)覆盖其行为。
  • 或者您实际上可以在调用之前使用 Moq 的虚拟方法 Setup。 (在这种情况下没有用,因为它会覆盖您实际要测试的方法。)
  • 或者您可以在调用方法之前说 p.CallBase = true;。其工作原理如下(Loose 行为):如果调用未设置的 virtual 成员,Moq 将调用基类的实现。

这解释了你所看到的。我同意 Sergey Berezovskiy 在他的回答中给出的建议。

【讨论】:

  • 选项 1 很好,但遗憾的是它与 NHibernate 不兼容,它会拒绝映射一个包含非虚拟方法的类。猜一堂课不能为两个大师服务:D 或者我真的可以尝试第三个选项,p.CallBase = true;,但我同意 Sergey 的回答;我应该更多地思考行为,是否应该被嘲笑
  • +1 感谢您分享我今天学到的新知识。 p.CallBase = true; 有效。但它不能代替批判性思维,我应该更多地思考什么和什么不应该模拟,以及如何进行有效的单元测试
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2019-06-21
  • 2015-06-24
  • 2017-12-19
  • 2010-12-26
  • 2012-12-31
  • 2010-09-27
  • 2015-06-25
相关资源
最近更新 更多