单元测试&构造函数依赖注入

时间:2012-10-15 13:20:11

标签: c# unit-testing dependency-injection ninject

我有一个关于如何设计适合单元测试的应用程序的问题。

我正在尝试实施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()方法是否是一个好习惯,或者以某种方式传递{{1}更有意义在获取RecurringProfile的实例时,对于DI框架,考虑到IRecurringProfileNextPaymentCreator将始终作用于IRecurringProfileNextPaymentCreator以创建下一笔付款。希望这会使问题更加明确。

2 个答案:

答案 0 :(得分:5)

在此类单元测试期间,您不应使用DI容器(Ninject)。在新建测试类时,您将手动注入模拟对象。然后验证是否在模拟上进行了调用。

答案 1 :(得分:4)

因为你没有显示任何代码我猜你想要做这样的事情

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 is considered an anti-pattern in modern application architecture。类似下面的代码应该可以正常工作。

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