【问题标题】:TDD can force the creation of "fake" dependenciesTDD 可以强制创建“假”依赖项
【发布时间】:2011-07-02 04:56:07
【问题描述】:

我在 ASP.NET WebForms 应用程序中使用 Model-View-Presenter 的样板实现。 My View 有两个后果事件,一个表明用户在域模型上填写了足够的字段以启动重复检查,另一个是常规的 Save 事件。我的伪代码如下所示:

public class ItemNewPresenter : PresenterBase<IItemNewView>
{
public IItemService Service { get; private set; }
public IItemNewView View { get; private set; }

public ItemNewPresenter(IItemService service, IItemNewView view)
{
    Service = service;
    View = view;
    View.OnSave += DoItemSave;
    View.OnItemIsDuplicateCheck+= DoItemIsDuplicateCheck;
}


private void DoItemIsDuplicateCheck(object sender, CheckItemDuplicateEventArgs e)
{
    CheckForItemDuplication(e.Item);
}

private void CheckForItemDuplication(Item item){

if (Service.IsDuplicateItem(item))
    {
        View.RedirectWithNotification(BuildItemUrl(item), "This item already exists");
    }
}
private void DoItemSave(object sender, SaveItemEventArgs e)
{
    DoItemIsDuplicateCheck(this, e.ToItemDuplicateEventArgs());
    Service.Save(e.Item);
}

}

这是我的测试,用于确保在从视图中引发 OnItemIsDuplicateCheck 时我的演示者行为正常:

[Test]
public void presenter_checking_for_existing_item_should_call_redirect_if_found()
{
    var service = new Mock<IItemService>();
    var view = new Mock<IItemNewView>();
    var presenter = new ItemNewPresenter (service.Object, view.Object);

    var onCheckExistingHandler = view.CreateEventHandler <CheckItemDuplicateEventArgs>();
    view.Object.OnExistingDenominatorCheck += onCheckExistingHandler;
    var eventArgs = new CheckItemDuplicateEventArgs();

    service.Setup(s => s.IsDuplicate(It.Is<CheckItemDuplicateEventArgs>(c => c.Equals(eventArgs)))).Returns(true);

    onCheckExistingHandler.Raise(eventArgs);

    view.Verify(v => v.RedirectWithNotification(It.IsAny<String>(), It.IsAny<string>()), Times.Once());
    service.Verify();
}

为了保持一致性,我希望在 View 引发 OnSave 事件时触发相同的重复检查。我的问题是,当我要验证的方法之一 (CheckForItemDuplication) 在被测类上声明时,我应该如何编写测试。验证 SUT 上的方法调用(错误)的替代方法是使用 lots 的重复代码编写我的保存测试(我的所有模拟的设置和断言将从上述测试中复制)并且它也使单元测试不那么集中。

   [Test]
    public void presenter_saving_item_should_check_for_dupe_and_save_if_not_one()    {
         //duplicate mocks/setups/asserts from duplicate check fixture
         //additional mocks/setups/asserts to test save logic
    }

认为 TDD 建议将此私有方法拉出到一个单独的类中,该类与我的 Presenter 协作并通过 DI 注入。但是为我的 Presenter 添加另一个依赖项以获取似乎不值得成为独立抽象的功能 *并且*表示我的 Presenter 的内部实现细节似乎......嗯......疯狂。我在这里离基地很远吗?必须有一些设计模式或重构我可以应用,以避免将私有方法转换为依赖项。

【问题讨论】:

    标签: c# unit-testing oop dependency-injection tdd


    【解决方案1】:

    我认为您陷入了 TDD 和信息隐藏之间永无止境的争论,因为您接受注入可能是正确的做法(而且很可能是),但也觉得外部交互不应该关心看似微不足道的注入。

    请不要因为我要说的话而对我投反对票:-)

    现在,当我遇到这种困境时,我有时会做的是提取函数,以对象作为参数创建一个内部构造函数,而没有一个公共构造函数。公共 ctor 使用新对象转发到内部,例如:

    public class ClassThatUseInjection
    {
        private readonly SomeClass _injectedClass;
    
        public ClassThatUseInjection(): this(new SomeClass()) {}
    
        internal ClassThatUseInjection(SomeClass injectedClass)
        {
            _injectedClass = injectedClass;
        }
    }
    
    
    public class SomeClass
    {
        public object SomeProperty { get; set; }
    }
    

    因此,您可以从外部使用空构造函数,而另一个构造函数用于当您想要注入存根参数以进行测试时。只要空的构造函数只转发调用而没有任何自己的逻辑,你仍然可以测试它,就像它只有一个构造函数一样。

    还是有点臭,是的,但不是臭臭的:-) 或者你怎么看?

    问候, 莫腾

    【讨论】:

    • 有趣的方法,但我觉得必须有更好的方法。正如您所指出的,我要测试的代码是 SUT 的私有实现细节,永远不需要通过消费类注入。引入一个新类来封装内部行为只是为了实现无重复测试代码似乎非常疯狂:(
    • 我同意德克的观点——这种治疗比疾病更糟糕。您想要测试一个类的事实不应对其设计产生负面影响。
    • 但您也可以争辩说,该类实际上不止做一件事:调解:作为模型和调解员,因此违反了单一责任政策。
    • @Dirk, @Phil:我不认为这很糟糕。出于测试原因分离并不意味着你总是想注入。您可以创建带有或不带有键比较器实现的字典。所以呢?注入可能会变得非常复杂,我建议在这方面有点务实,如果涉及内部细节,也提供默认实现。
    • 莫腾,我不太了解 SRP 违规。演示者不是要在视图层和服务层之间进行调解。我对引入新课程的想法持开放态度,因此期待您的回复。
    【解决方案2】:

    我会通过添加重复的设置代码来测试该类。一旦该测试通过并且您确信涵盖了所有测试用例,您就可以重构您的测试代码以消除重复。

    您可以将依赖项(服务和视图)移动到私有字段,然后添加一个方法来创建 SUT:

    private Mock<IItemService> _service;
    private Mock<IItemNewView> _view;
    
    private PresenterBase<IItemNewView> CreateSUT()
    {
        _service = new Mock<IItemService>();
        _view = new Mock<IItemNewView>();
        return new ItemNewPresenter (service.Object, view.Object);
    }
    

    (我认为大多数人更愿意在 Setup 方法中初始化 Mock 对象。)

    从您的测试中调用 CreateSUT,现在重复减少了一点。然后,您可能希望添加私有方法来创建事件处理程序/引发事件,只要它在多个测试用例中执行相同或相似。

    拥有这个 CreateSUT 方法可以减少调用构造函数的测试代码的数量,从而在将来添加/删除/更改依赖项时更容易。如果您像对待任何其他代码一样对待您的测试代码,并在看到重复时使用 DRY 原则,它可以导致更明确、更易于阅读、可维护的测试代码。处理非常相似的设置和测试上下文是单元测试的常见问题,不应总是改变被测试类的设计方式。

    【讨论】:

      【解决方案3】:

      如果有更好的答案,我会很感兴趣,因为我经常遇到这种情况。

      验证 SUT 上的方法调用(不好)的替代方法是使用大量重复代码编写我的保存测试(我的所有模拟的设置和断言将从上述测试中复制),它也使单元测试不那么集中。

      我不确定您为什么觉得它会降低测试的重点,但在您的情况下,我会做您不想做的事——重复设置代码来测试 SUT 的孤立案例.您正在使用您提供的测试来测试 SUT 的外部行为,这对我来说似乎完全正确。

      我个人不喜欢从类中公开超出必要的内容和/或将应由 SUT 负责的行为变成依赖项,以方便测试。不应仅仅因为您想对其进行测试而违反该类职责的“自然边界”。

      【讨论】:

        【解决方案4】:

        对 url 的计算进行单元测试比对发生重定向进行单元测试更容易。

        如果我正确地理解你,你想测试 mvp-s CheckForItemDuplication() 通过提升重定向到某个 url view-mock-s OnItemIsDuplicateCheck 事件。

        private void CheckForItemDuplication(Item item)
        {
            if (Service.IsDuplicateItem(item))
            {
                View.RedirectWithNotification(BuildItemUrl(item), 
                               "This item already exists");
            }
        }
        

        在我看来,你做的太多了。 如果您将代码重写为

        internal protected GetErrorUrlForItem(Item item)
        {
            if (Service.IsDuplicateItem(item))
            {
                return BuildItemUrl(item, 
                                    "This item already exists");
            }
            return null;
        }
        
        private void CheckForItemDuplication(Item item)
        {
            var result = GetErrorUrlForItem(item);
            if (result != null)
            {
                View.RedirectWithNotification(result);
            }
        }
        

        在单元测试中只测试内部方法GetErrorUrlForItem()。您必须使用InternalsVisibleTo 属性来允许访问内部方法。

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2011-11-18
          • 1970-01-01
          • 1970-01-01
          • 2011-10-24
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多