簡體   English   中英

單元測試和構造函數依賴注入

[英]unit testing & constructor dependency injection

關於如何設計適合單元測試的應用程序,我遇到了一個問題。

我正在嘗試實施SRP(單一責任原則),據我所知,這涉及在單獨的專用類中拆分大多數功能以保持代碼更有條理。 例如,我有這個特定的場景。

一個RecurringProfile類,它有一個方法.ActivateProfile() 此方法的作用是將狀態標記為已激活,並為下一個截止日期創建下一個(第一個)定期付款。 我打算拆分功能,在單獨的類中創建下一個定期付款,例如RecurringProfileNextPaymentCreator 我的想法是讓這個類將'RecurringProfile'作為其構造函數中的參數:

RecurringProfileNextPaymentCreator(IRecurringProfile profile)

但是,我認為這對於單元測試來說會有問題。 我想創建一個測試ActivateProfile功能的單元測試。 此方法將通過依賴注入(Ninject)獲取IRecurringProfileNextPaymentCreator的實例,並調用方法.CreateNextPayment

我創建單元測試的想法是創建一個IRecurringProfileNextPaymentCreator的模擬,並替換它以便我可以驗證.ActivateProfile()實際上調用了該方法。 但是,由於構造函數參數,這不適合作為NInject的默認構造函數。 必須為這種情況創建一個自定義的NInject提供程序(我可以在整個解決方案中擁有許多這樣的類)將有點矯枉過正。

任何想法/最佳實踐如何進行此操作?

- 下面是關於上述示例的示例代碼:(請注意,代碼是手寫的,在語法上不是100%正確)

public class RecurringProfile
{
    public void ActivateProfile()
    {
        this.Status = Enums.ProfileStatus.Activated;
        //now it should create the first recurring payment
        IRecurringProfileNextPaymentCreator creator = NInject.Get<IRecurringProfileNextPaymentCreator>();
        creator.CreateNextPayment(this); //this is what I'm having an issue about 
    }
}

一個樣本單元測試:

public void TestActivateProfile()
{   
    var mockPaymentCreator = new Mock<IRecurringProfileNextPaymentCreator>();
    NInject.Bind<IRecurringProfileNextPaymentCreator>(mockPaymentCreator.Object);

    RecurringProfile profile = new RecurringProfile();
    profile.ActivateProfile();
    Assert.That(profile.Status == Enums.ProfileStatus.Activated);
    mockPayment.Verify(x => x.CreateNextPayment(), Times.Once());

}

回到示例代碼,我的問題是將RecurringProfile作為參數傳遞給creator.CreateNextPayment()方法是否是一個好習慣,或者以某種方式將RecurringProfile傳遞給DI框架更有意義,在獲取IRecurringProfileNextPaymentCreator的實例時,考慮到IRecurringProfileNextPaymentCreator將始終作用於IRecurringProfile以創建下一個付款。 希望這會使問題更加明確。

在此類單元測試期間,您不應使用DI容器(Ninject)。 在新建測試類時,您將手動注入模擬對象。 然后驗證是否在模擬上進行了調用。

因為你沒有顯示任何代碼,我猜你想要做這樣的事情

public class RecurringProfile
{
  private readonly DateTime _dueDate;
  private readonly TimeSpan _interval;
  public RecurringProfile(DateTime dueDate, TimeSpan interval)
  {
    _dueDate = dueDate;
    _interval = interval;
  }
  public bool IsActive { get; private set; }
  public DateTime DueDate
  {
    get { return _dueDate; }
  }
  public TimeSpan Interval
  {
    get { return _interval; }
  }
  public RecurringProfile ActivateProfile()
  {
    this.IsActive = true;
    return new RecurringProfile(this.DueDate + this.Interval, this.Interval);
  }
}

這不夠簡單嗎?


更新

不要濫用DI容器作為ServiceLocator。 您將付款創建者注入ctor參數的想法是正確的方法。 ServiceLocator被認為是現代應用程序架構中的反模式 類似下面的代碼應該工作正常。

[TestClass]
public class UnitTest1
{
  [TestMethod]
  public void TestMethod1()
  {
    var mock = new Mock<INextPaymentCreator>();
    DateTime dt = DateTime.Now;
    var current = new RecurringProfile(mock.Object, dt, TimeSpan.FromDays(30));
    current.ActivateProfile();
    mock.Verify(c => c.CreateNextPayment(current), Times.Once());
  }
}
public class RecurringProfile
{
  private readonly INextPaymentCreator _creator;
  private readonly DateTime _dueDate;
  private readonly TimeSpan _interval;
  public RecurringProfile(INextPaymentCreator creator, DateTime dueDate, TimeSpan interval)
  {
    _creator = creator;
    _dueDate = dueDate;
    _interval = interval;
  }
  public bool IsActive { get; private set; }
  public DateTime DueDate
  {
    get { return _dueDate; }
  }
  public TimeSpan Interval
  {
    get { return _interval; }
  }
  public RecurringProfile ActivateProfile()
  {
    this.IsActive = true;
    var next = this._creator.CreateNextPayment(this);
    return next;
  }
}

public interface INextPaymentCreator
{
  RecurringProfile CreateNextPayment(RecurringProfile current);
}

暫無
暫無

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

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