2009-08-31 47 views
0

首先,我要说的是,尽管是TDD的一名相当新的从业者,但我几乎可以从中受益。我觉得我已经有足够的进步来考虑使用嘲讽,并且在了解嘲笑与OOP适合的地方时碰到了一面真正的砖墙。关于嘲讽的另一个问题

我读过尽可能多的相关文章/文章,我可以找到(Fowler,Miller),我仍然不完全清楚如何或何时模拟。

让我举一个具体的例子。我的应用程序有一个服务层类(有些人称之为应用层?),其中方法大致映射到特定用例。这些类可以与持久层,域层甚至其他服务类协作。我是一个善良的小男孩DI和已正确分解出来我的依赖,使他们能够底涂用于测试目的等

样本服务类可能是这样的:

public class AddDocumentEventService : IAddDocumentEventService 
{ 
    public IDocumentDao DocumentDao 
    { 
     get { return _documentDao; } 
     set { _documentDao = value; } 
    } 
    public IPatientSnapshotService PatientSnapshotService 
    { 
     get { return _patientSnapshotService; } 
     set { _patientSnapshotService = value; } 
    } 

    public TransactionResponse AddEvent(EventSection eventSection) 
    { 
     TransactionResponse response = new TransactionResponse(); 
     response.Successful = false; 

     if (eventSection.IsValid(response.ValidationErrors)) 
     { 

      DocumentDao.SaveNewEvent(eventSection, docDataID); 

      int patientAccountId = DocumentDao.GetPatientAccountIdForDocument(docDataID); 
      int patientSnapshotId =PatientSnapshotService.SaveEventSnapshot(patientAccountId, eventSection.EventId); 

      if (patientSnapshotId == 0) 
      { 
       throw new Exception("Unable to save Patient Snapshot!"); 
      } 

      response.Successful = true; 
     } 
     return response; 
    } 

}

我使用NMock完成了对其方法(DocumentDao,PatientSnapshotService)进行隔离测试的过程。这里的测试看起来像

[Test] 
public void AddEvent() 
    { 
     Mockery mocks = new Mockery(); 
     IAddDocumentEventService service = new AddDocumentEventService(); 
     IDocumentDao mockDocumentDao = mocks.NewMock<IDocumentDao>(); 
     IPatientSnapshotService mockPatientSnapshot = mocks.NewMock<IPatientSnapshotService>(); 

     EventSection eventSection = new EventSection(); 

     //set up our mock expectations 
     Expect.Once.On(mockDocumentDao).Method("GetPatientAccountIdForDocument").WithAnyArguments(); 
     Expect.Once.On(mockPatientSnapshot).Method("SaveEventSnapshot").WithAnyArguments(); 
     Expect.Once.On(mockDocumentDao).Method("SaveNewEvent").WithAnyArguments(); 

     //pass in our mocks as dependencies to the class under test 
     ((AddDocumentEventService)service).DocumentDao = mockDocumentDao; 
     ((AddDocumentEventService)service).PatientSnapshotService = mockPatientSnapshot; 

     //call the method under test 
     service.AddEvent(eventSection); 

     //verify that all expectations have been met 
     mocks.VerifyAllExpectationsHaveBeenMet(); 
    } 

我对这个小涉足嘲讽的想法是什么如下:

  1. 这个测试的出现打破许多基本OO戒律,而不是其中最重要的就是封装:我测试深入了解被测试类的具体实现细节(即方法调用)。每当课程内部发生变化时,我都会发现很多无用的时间用于更新测试。
  2. 也许是因为我的服务类目前相当简单,但我不太明白这些测试增加了什么价值。是否我保证协作对象正在按照特定用例的要求被调用?代码重复似乎很荒谬,因为这样一个小小的好处。

我错过了什么?

+0

“我缺少什么?” - 测试重构以消除冗余代码?即将常用步骤移到您的测试设置中。 – tvanfosson 2009-08-31 18:11:18

回答

2

你提到了一个很好的帖子,来自martin fowler关于这个问题。他提到的一点是,仿冒者是喜欢测试行为并孤立东西的人。

传统的TDD风格是使用真实物体,如果可能的话使用真实物体,如果使用真实物体时使用双重物体,那么传统的TDDer会使用真正的仓库和双倍的邮件服务。没有真正的问题太多。

一个mockist TDD从业者,但是,将始终使用一个模拟与有趣的行为的任何对象。在这种情况下,对仓库和邮件服务并重。

如果你不喜欢这种东西,你可能是一个经典的TDDer,只有当它使用mock时很尴尬(像邮件服务,或收取信用卡)。 否则,您创建自己的双打(如创建内存数据库)。

特别是,我是一个模拟器,但是我不确定是否调用了特定的方法(除非它不返回值)。无论如何,我会测试接口。当函数返回时,我使用模拟框架来创建存根。

最后,这一切都在你想要测试什么以及如何测试。你认为检查这些方法是否真的被称为(使用模拟)很重要吗?您是否想在通话前后检查状态(使用假货)? 选择足够的东西来考虑它正在工作,然后构建你的测试来检查它!

关于测试的价值,我有一些意见:

  • 在短期内,当你TDD你通常得到一个更好的设计,虽然你可能会需要更长的时间。
  • 在长期,你也不会太害怕改变和维护该代码后(当你不会记得很好的细节),你会得到一个红色的马上,几乎即时反馈

顺便说一句,测试代码大小与生产代码大小一样大是正常的。

+0

“[snip]我不确定是否调用了特定的方法(除非它不返回值)。无论如何,我会测试接口。[snip]” 我猜我没有认为测试方法调用的对象的存根/嘲讽是可选的。例如,在上面提供的示例场景中,如果我没有为胶合对象提供存根或实际实现,测试不会失败吗? – 2009-09-04 00:18:06

0

我发现这些类型的测试有一个假的(内存)持久层是有用的;然后,而不是验证是否进行了某些调用,您可以验证最终结果(项目现在存在于存储库中)。我知道你正在使用mock,但我想我说我不认为这是最好的地方。

举个例子,伪码,这个测试我会看到如下:

Instantiate the fake repositories. 
Run your test method. 
Check the fake repository to see if the new elements exist in it. 

这样可以使你的测试,不知道实现细节的。然而,它的确意味着维护假的持久层,所以我认为这是一个折衷,你必须考虑。

1

打破封装,从而使您的测试更紧密地耦合到您的代码可以肯定是使用模拟的缺点。你不想让你的测试对重构变得脆弱。这是你必须走的一条细线。我个人避免使用mock,除非它非常困难,尴尬或缓慢。看看你的代码,首先,我会使用BDD风格:你的测试方法应该测试方法的特定行为,并且应该这样命名(可能类似于AddEventShouldSaveASnapshot)。其次,经验法则是只验证预期的行为发生,而不是编目应该发生的每一个方法调用。

+0

听起来很有趣。我如何验证我包含的样本中的预期行为,而不为对象的合作者提供嘲讽?就像克里斯在下面的答案中所描述的一样? – 2009-09-04 00:13:38

0

在代码中分离利益相关者有时是值得的。

封装是关于以最高的变化传播概率最小化潜在依赖关系的数量。这是基于静态源代码。

单元测试是关于确保运行时行为不会无意中更改:它不基于静态源代码。

当单元测试人员不是针对原始封装的源代码,而是将其所有私有访问者自动更改为公共访问者的源代码副本时(这只是一个四行shell脚本)。

这干净地分开encapsuation和单元测试。

那么在你的单元测试中只剩下你有多低:你想测试多少种方法。这是一个品味问题。

更多关于封装(但没有单元测试),请参阅: http://www.edmundkirwan.com/encap/overview/paper7.html

1

与嘲笑应该帮助您了解合作对象—描述他们应该如何相互通信的协议之间的关系的测试。在这种情况下,您想知道事件到达时会有几件事情会持续下去。如果你正在描述一个对象的外部关系,这并不是破坏封装。 DAO和服务类型描述这些外部服务,但不定义它们如何实现。

这只是一个小例子,但代码感觉程序而不是OO。有几种简单的值从一个对象中提取并传递给另一个对象的情况。也许某种Patient对象应该直接处理事件。很难说,但也许测试暗示这些设计问题?

在此期间,如果你不介意的自我宣传和可以再等一个月, http://www.growing-object-oriented-software.com/

1

我有同样不安的感觉,当我写这样的测试。当我通过将期望值复制到函数体中来实现函数时(特别是当我使用LeMock进行模拟时),它尤其让我感到震惊。

但它确定。它发生了。此测试现在记录并验证被测系统如何与其依赖关系交互,这是一件好事。此测试还有其他问题:

  1. 它一次测试过多。该测试验证三个依赖关系是否正确调用。如果这些依赖关系中的任何一个发生变化,这个测试将不得不改变。最好有3个独立的测试,验证每个依赖关系是否得到妥善处理。传入一个存根对象,用于未测试的依赖关系(而不是模拟对象,因为它会失败)。

  2. Theres没有验证传递给依赖项的参数,所以这些测试是不完整的。

在本示例中,我将使用Moq作为嘲讽库。这个测试没有指定所有依赖的行为,它只测试一个调用。它还将检查传入的参数是否是给定输入的期望值,输入的变化将证明单独的测试是合理的。

public void AddEventSavesSnapshot(object eventSnaphot) 
{ 
    Mock<IDocumentDao> mockDocumentDao = new Mock<IDocumentDao>(); 
    Mock<IPatientSnapshotService> mockPatientSnapshot = new Mock<IPatientSnapshotService>(); 

    string eventSample = Some.String(); 
    EventSection eventSection = new EventSection(eventSample); 

    mockPatientSnapshot.Setup(r => r.SaveEventSnapshot(eventSample)); 

    AddDocumentEventService sut = new AddDocumentEventService(); 
    sut.DocumentDao = mockDocumentDao; 
    sut.PatientSnapshotService = mockPatientSnapshot; 

    sut.AddEvent(eventSection); 

    mockPatientSnapshot.Verify(); 
} 

请注意,如果AddEvent()可能使用它们,那么仅在此测试中才需要传入未使用的依赖项。相当合理地,该类可能具有与此测试未涉及的相关性。

+0

感谢您的反馈。你能否详细说明你提出的两大要点。如果你有时间,也许会用一些伪代码。 – 2010-01-05 19:10:54