簡體   English   中英

單元測試富域模型

[英]Unit Testing 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 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>());
}

但是,我想練習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;
    }           
}

但是我遇到測試問題:

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

看到直接從模擬對象的Object調用方法后,模擬對象便無法檢測其屬性或方法是否被調用。 注意到這一點之后,對富域模型進行單元測試的正確方法是什么?

首先, 不要嘲笑您正在測試的值對象或類。 另外,您沒有驗證是否已向人員提供了正確的修改日期。 您檢查是否已分配一些日期。 但這不能證明您的代碼可以按預期工作。 為了測試這樣的代碼,您應該模擬 DateTime.Now返回的當前日期 ,或者創建一些抽象方法 ,以提供當前服務時間。 您的第一個測試應該看起來像(我在這里使用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();
}

您的來電:

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

在您的模擬上調用公共virtual方法UpdatePerson 模擬對象的行為為Loose (也稱為Default ),並且您沒有Setup該虛擬方法。

在這種情況下,Moq的行為是在UpdatePerson實現(重寫)中什么也不做。

您可以通過多種方式更改此設置。

  • 您可以從UpdatePerson方法中刪除virtual關鍵字。 然后Moq將不會(也不能)覆蓋其行為。
  • 或者,您實際上可以在調用Moq之前Setup虛擬方法。 (在這種情況下沒有用,因為它會覆蓋您實際要測試的方法。)
  • 或者你可以說p.CallBase = true; 在調用該方法之前。 它的工作方式如下(行為Loose ): 如果調用了未設置的virtual成員,則Moq將調用基類的實現。

這解釋了您所看到的。 我可以同意謝爾蓋·貝雷佐夫斯基在回答中提出的建議。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM