首先让我说,尽管TDD是一位相当新的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();
}
我对这种嘲弄的小想法的想法如下:
我错过了什么?
答案 0 :(得分:2)
你提到马丁福勒关于这个问题的一篇非常好的帖子。他提到的一点是,嘲笑者喜欢测试行为并隔离东西。
“经典的TDD风格是尽可能使用真实对象,如果使用真实物品则很难使用双重。所以经典的TDDer会使用真正的仓库和双重的邮件服务。双重并不重要。
然而,模仿者TDD从业者将始终对任何有趣行为的对象使用模拟。在这种情况下,仓库和邮件服务。“
如果你不喜欢这种东西,你可能是一个经典的TDDer,只有当它很笨拙时才应该使用模拟(如邮件服务或信用卡收费)。 否则,您创建自己的双打(如创建内存数据库)。
特别是,我是一个模仿者,但是如果调用特定方法(除非它不返回值),我不会验证多少。在任何情况下,我都会测试接口。当函数返回一些东西时,我使用mocking框架来创建存根。
最后,这一切都来自于您想要测试的内容和方式。您认为检查这些方法是否真的被调用(使用模拟)是否很重要?你想只是检查通话前后的状态(使用假货)吗? 选择足以认为它正常工作的内容,然后构建测试以确切地检查它!
关于测试的价值,我有一些看法:
顺便说一下,测试代码大小与生产代码大小一样大是正常的。
答案 1 :(得分:1)
破坏封装并因此使您的测试与代码紧密耦合绝对是使用模拟的缺点。您不希望您的测试对重构很脆弱。这是一个你必须走的细线。我个人避免使用模拟,除非它真的很难,笨拙或慢。 看看你的代码,首先,我会使用BDD样式:你的测试方法应该测试方法的特定行为,并且应该这样命名(可能像AddEventShouldSaveASnapshot)。其次,经验法则是仅验证预期的行为是否发生,而不是对应该发生的每个方法调用进行编目。
答案 2 :(得分:1)
使用模拟测试可以帮助您理解协作对象之间的关系 - 描述它们应该如何相互通信的协议。在这种情况下,您希望知道在事件到达时会保留一些内容。如果您描述对象的外部关系,那么它不会破坏封装。 DAO和服务类型描述了那些外部服务,但没有定义它们的实现方式。
这只是一个小例子,但代码感觉是程序性而不是OO。有几种简单值从一个对象中提取并传递给另一个对象。也许某种Patient对象应该直接处理事件。很难说,但也许测试暗示了这些设计问题?
与此同时,如果你不介意自我推销,可以再等一个月, http://www.growing-object-oriented-software.com/
答案 3 :(得分:1)
当我写这样的测试时,我有同样的不安感。当我通过copy'n'pasting函数体中的期望来实现函数时(特别是当我使用LeMock进行模拟时),它特别让我感到震惊。
但是没关系。它发生了。此测试现在记录并验证被测系统如何与其依赖项交互,这是一件好事。这个测试还有其他问题:
一次测试太多了。此测试验证是否正确调用了三个依赖项。如果这些依赖项中的任何一个发生更改,则此测试必须更改。最好有3个单独的测试,验证每个依赖项是否得到妥善处理。传入一个存根对象,查找未测试的依赖项(而不是模拟,因为它会失败)。
没有验证传递给依赖项的参数,因此这些测试不完整。
在此示例中,我将使用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()可能使用它们,则只在此测试中需要传入的未使用的依赖项。相当合理的是,该类可能具有与未显示的此测试无关的依赖项。
答案 4 :(得分:0)
我发现在这些类型的测试中使用伪(内存)持久层很有用;然后,您可以验证最终结果(项目现在存在于存储库中),而不是验证是否进行了某些调用。我知道你正在使用嘲讽,但我想我说我不认为这是最好的地方。
作为一个例子,我将看到这个测试的伪代码:
Instantiate the fake repositories.
Run your test method.
Check the fake repository to see if the new elements exist in it.
这使您的测试不会意识到实现细节。但是,它确实意味着维护假持久层,所以我认为这是一个你需要考虑的权衡。
答案 5 :(得分:0)
将代码中的利益相关者分开有时是值得的。
封装是关于最小化具有最高变化传播概率的潜在依赖性的数量。这是基于静态源代码。
单元测试是为了确保运行时行为不会无意中发生变化:它不是基于静态源代码。
当单元测试人员不使用原始的封装源代码,而是在源代码的副本上工作时,它有时很有用,源代码的所有私有访问器都自动更改为公共访问器(这只是一个四行的shell脚本)。
这将封装与单元测试完全分开。
然后只剩下你的单元测试有多低:你想测试多少种方法。这是一个品味问题。
有关封装的更多信息(但在单元测试中没有任何内容),请参阅: http://www.edmundkirwan.com/encap/overview/paper7.html