2011-02-22 42 views
4

我在ASP.NET WebForms应用程序中使用Model-View-Presenter的样板实现。我的视图有两个结果事件,一个表示用户在域模型上填充了足够的字段以启动重复检查,另一个是常规的保存事件。我的伪代码如下所示:TDD可以强制创建“假”依赖关系

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方法调用(坏)的替代方法是用重复代码编写我的保存测试批次(我的所有嘲笑的设置和断言都将从上述测试中复制),并且还会使单元测试不那么专注。

[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建议拉这个私有方法到一个单独的类,它与我的演讲合作,并会通过DI注入。但是,向我的Presenter添加另一个依赖关系,看起来不值得作为独立抽象的功能*和*表示我的Presenter的内部实现细节,看起来......好吧......太疯狂了。我在这里基地?必须有一些设计模式或重构我可以应用,这将避免需要将私有方法变成依赖项。

回答

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

(我想大多数人宁愿在设置方法来初始化Mock对象)

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

有了这个CreateSUT方法,可以减少调用构造函数的测试代码的数量,以便将来添加/删除/更改依赖关系。如果您将测试代码像任何其他代码一样对待,并且在看到重复时使用DRY原则,则可以更清晰,更易于阅读和维护测试代码。处理非常相似的设置和测试环境是单元测试的常见问题,不应该总是改变被测试类的设计方式。

5

我觉得你陷入了TDD和信息隐藏之间永无止境的争论中,因为你接受注射可能是正确的事情(可能是),但也认为外界的交流不应该在乎关于一个看似不重要的注射。

请不要向下投我为我smellyness什么,我要说的话:-)

现在,我有时会做,当这种尴尬局面的面对,是提取功能,使用该对象作为参数创建一个内部构造函数,并且不带一个公共构造函数。公共构造函数被转发到内部用一个新的对象,如:

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一个存根参数中使用来自外部的空的构造,以及其他的构造函数。只要空的构造函数只转发没有任何逻辑的调用,你仍然可以测试它,就像它只有一个构造函数一样。

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

问候, 莫滕

+1

有趣的方法,但我觉得必须有更好的方法。正如你所指出的,我试图测试的代码是SUT的私有实现细节,并且永远不需要通过消费类注入。引入一个新的类来封装内部行为只是为了实现免费的测试代码似乎彻头彻尾的疯狂:( – 2011-02-26 17:22:55

1

,如果有更好的答案,我会感兴趣,因为我遇到这一切的时候。

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

我不确定为什么你觉得它使得测试不那么集中,但在你的鞋子里,我会做它听起来像你不想做的事 - 重复设置代码来测试孤立的情况下SUT。你正在用你提供的测试来测试SUT的外部行为,这对我来说似乎完全正确。

我个人并不是一个暴露超过从一个类和/或行为应该是SUT的责任进入一个依赖只是为了方便测试的必要。不应仅仅因为你想测试它而违反班级责任的“自然边界”。

1

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

如果我corretly了解你要测试的MVP-S CheckForItemDuplication()通过提高 视图 - 模拟-S OnItemIsDuplicateCheck事件重定向到特定URL中。

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属性来允许访问内部方法。