【问题标题】:unit testing & constructor dependency injection单元测试和构造函数依赖注入
【发布时间】:2012-10-15 13:20:11
【问题描述】:

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

我正在尝试实施 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 是否更有意义-framework,在获取IRecurringProfileNextPaymentCreator 的实例时,考虑到IRecurringProfileNextPaymentCreator 将始终作用于IRecurringProfile 以创建下一次付款。希望这能让问题更清楚一点。

【问题讨论】:

  • @KellyEthridge 将此作为答案而不是评论

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


【解决方案1】:

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

【讨论】:

  • @KellyEhtridge “手动注入”到底是什么意思?也许您可以提供一个 code-sn-p / 示例来提供一个想法?据我所知,DI 的主要作用之一是能够用模拟对象替换对象以进行单元测试。
  • @SebastianWeber 这是一本有趣的书——但是,我的主要问题并不完全是这样。穷人的 DI 解释了在构造函数中有具体的实现。就我而言,我的问题是是否应该创建实现IRecurringProfileNextPaymentCreator' with a constructor taking the IRecurringProfile` 的类并以某种方式传递它,或者方法IRecurringProfileNextPaymentCreator.CreateNextPayment() 是否应该将IRecurringProfile 作为方法的参数。
  • @KarlCassar 我只是在回答你的问题“'手动注入'到底是什么意思?”
【解决方案2】:

由于你没有显示任何代码,我猜你想做这样的事情

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

【讨论】:

  • 我没有发布任何代码,因为代码尚未开发,这更像是一个设计/最佳实践问题。我仍在掌握 IoC/DI 原则 - 我将使用一些示例代码重新编辑原始帖子,以更好地了解我要问的内容。
  • 感谢您的洞察力。我正在阅读有关 Service Locator 反模式和其他 DI 反模式的更详细信息,看来我走错了路,这看起来更整洁,现在更有意义。老实说,我已经通过 Service-Locator 模式找到了“自动”解决依赖关系的方法,我使用这种模式令人困惑,因为它完全隐藏了依赖关系,现在已经清除了它:)
猜你喜欢
  • 2011-05-19
  • 2015-05-12
  • 2013-03-23
  • 1970-01-01
  • 1970-01-01
  • 2011-02-02
  • 2018-03-03
  • 2021-06-19
  • 1970-01-01
相关资源
最近更新 更多