TDD可以强制创建“虚假”​​依赖项

时间:2011-02-22 18:03:52

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

我在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上的方法调用(坏)的替代方法是使用重复代码的 lot 编写我的保存测试(我的所有模拟的设置和断言将从上面的测试中复制)并且它也使单元测试不那么集中。

   [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的内部实现细节似乎......好吧......疯了。我离开基地吗?必须有一些我可以应用的设计模式或重构,以避免将私有方法转换为依赖。

4 个答案:

答案 0 :(得分:5)

我认为你陷入了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; }
}

因此,您可以使用外部的空构造函数,以及其他构造函数,以便为testpurposes注入存根参数。只要空构造函数只转发调用而没有任何自己的逻辑,你仍然可以测试它,就像它只有一个构造函数一样。

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

此致 的Morten

答案 1 :(得分:1)

如果有更好的答案,我会感兴趣,因为我一直都会遇到这种情况。

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

我不确定为什么你觉得它让测试不那么专注,但是你会觉得你听起来不想做什么 - 有重复的设置代码来测试SUT的孤立案例。您正在使用您提供的测试来测试SUT的外部行为,这对我来说是完全正确的。

我个人不喜欢暴露超出必要的类和/或使行为应该是SUT的责任,只是为了便于测试。不应仅仅因为你想测试它而违反了班级责任的“自然界限”。

答案 2 :(得分:1)

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

如果我理解你,你想要测试mvp-s CheckForItemDuplication()通过提高来重定向到某个网址 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);
    }
}

在unittest中只测试内部方法GetErrorUrlForItem()。您必须使用InternalsVisibleTo属性才能访问内部方法。

答案 3 :(得分:1)

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

您可以将依赖项(服务和视图)移动到私有字段,然后添加一个方法来创建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原则,则可以生成更明确,更易于阅读,可维护的测试代码。处理非常相似的设置和测试上下文是单元测试的常见问题,并且不应总是改变被测试类的设计方式。