我是正确的测试驱动设计或行为驱动设计的大力倡导者,我喜欢编写测试。然而,我一直把自己编码到一个角落,我需要在一个特定的测试用例中为一个类使用 3-5 个模拟。无论我以哪种方式开始,自上而下或自下而上,我最终的设计都需要来自最高抽象级别的至少三个协作者。
有人可以就如何避免这个陷阱给出好的建议吗?
这是一个典型的场景。我设计了一个从给定文本值生成 Midget 的 Widget。在我进入细节之前,它总是开始非常简单。我的 Widget 必须与一些难以测试的东西交互,比如文件系统、数据库和网络。
因此,我没有将所有这些都设计到我的 Widget 中,而是创建了一个 Bridget 合作者。 Bridget 处理了一半的复杂性,即数据库和网络,让我可以专注于另一半,即多媒体演示。所以,然后我制作了一个执行多媒体片段的小工具。整个事情需要在后台发生,所以现在我包括一个 Thridget 来实现它。当一切都说完了,我最终得到了一个小部件,它将工作交给一个 Thridget,它通过一个 Bridget 将其结果提供给一个 Gidget。
因为我在 CocoaTouch 中工作并试图避免模拟对象,所以我使用自分流模式,其中对协作者的抽象成为我的测试采用的协议(protocol)。与 3 个以上的合作者一起,我的测试气球变得太复杂了。即使使用 OCMock 模拟对象之类的东西,也会给我留下一个我宁愿避免的复杂性顺序。我尝试将我的大脑包裹在一个菊花链的协作者(A 代表 B,B 代表 C 等等),但我无法想象它。
编辑
以下面的示例为例,假设我们有一个必须从套接字读取/写入并显示返回的电影数据的对象。
//Assume myRequest is a String param...
InputStream aIn = aSocket.getInputStram();
OutputStream aOut = aSocket.getOutputStram();
DataProcessor aProcessor = ...;
// This gets broken into a "Network" collaborator.
for(stuff in myRequest.charArray()) aOut.write(stuff);
Object Data = aIn.read(); // Simplified read
//This is our second collaborator
aProcessor.process(Data);
现在上面显然处理网络延迟,所以它必须是线程的。这引入了一个线程抽象来让我们摆脱线程单元测试的实践。我们现在有
AsynchronousWorker myworker = getWorker(); //here's our third collaborator
worker.doThisWork( new WorkRequest() {
//Assume myRequest is a String param...
DataProcessor aProcessor = ...;
// Use our "Network" collaborator.
NetworkHandler networkHandler = getNetworkHandler();
Object Data = networkHandler.retrieveData(); // Simplified read
//This is our multimedia collaborator
aProcessor.process(Data);
})
请原谅我在没有测试的情况下向后工作,但我正要带我的女儿出去,我正在匆匆完成这个例子。这里的想法是,我正在从一个简单的界面后面协调几个协作者的协作,该界面将与 UI 按钮单击事件相关联。所以最外面的测试反射(reflect)了一个 Sprint 任务,它说给定一个“播放电影”按钮,当它被点击时,电影就会播放。
编辑
让我们讨论。
最佳答案
拥有许多模拟对象表明:
1)你有太多的依赖。
重新查看您的代码并尝试进一步分解它。特别是,尽量将数据转换和处理分开。
由于我在您正在开发的环境中没有经验。所以让我以我自己的经验为例。
在 Java 套接字中,您将获得一组简单的 InputStream 和 OutputStream,以便您可以从对等点读取数据并将数据发送到对等点。所以你的程序看起来像这样:
InputStream aIn = aSocket.getInputStram();
OutputStream aOut = aSocket.getOutputStram();
// Read data
Object Data = aIn.read(); // Simplified read
// Process
if (Data.equals('1')) {
// Do something
// Write data
aOut.write('A');
} else {
// Do something else
// Write another data
aOut.write('B');
}
如果你想测试这个方法,你最终必须为 In 和 Out 创建 mock,这可能需要它们背后的相当复杂的类来支持。
但是如果你仔细看,从 aIn 中读取和向 aOut 中写入是可以分开处理的。因此,您可以创建另一个类,该类将接受读取输入并返回输出对象。
public class ProcessSocket {
public Object process(Object readObject) {
if (readObject.equals(...)) {
// Do something
// Write data
return 'A';
} else {
// Do something else
// Write another data
return 'B';
}
}
你以前的方法是:
InputStream aIn = aSocket.getInputStram();
OutputStream aOut = aSocket.getOutputStram();
ProcessSocket aProcessor = ...;
// Read data
Object Data = aIn.read(); // Simplified read
aProcessor.process(Data);
这样您就可以在几乎不需要模拟的情况下测试处理。你测试可以去:
ProcessSocket aProcessor = ...;
assert(aProcessor.process('1').equals('A'));
因为处理现在独立于输入、输出甚至套接字。
2)您通过单元测试完成了单元测试,应该进行集成测试。
有些测试不适用于单元测试(从某种意义上说,它需要不必要的更多努力,并且可能无法有效地获得一个好的指标)。这类测试的示例是那些涉及并发和用户界面的测试。它们需要与单元测试不同的测试方式。
我的建议是你进一步分解它们(类似于上面的技术),直到其中一些适合单元测试。所以你有一些难以测试的部分。
编辑
如果你相信你已经把它分解成非常细的部分,也许这就是你的问题。
软件组件或子组件以某种方式相互关联,例如字符组合成词,词组合成句子,句子组合成段落,段落组合成小节,章节,章节等等。
我的例子说,你应该将小节分成段落,而你已经将事情归结为单词。
这样看,大多数时候,段落与其他段落的关联程度比与其他句子相关(或依赖于)其他句子的句子松散程度要低。小节、小节更加松散,而单词和字符则更加依赖(随着语法规则的出现)。
因此,也许您将其破坏得如此之好,以至于语言语法强制这些依赖项,进而迫使您拥有如此多的模拟对象。
如果是这种情况,您的解决方案是平衡测试。如果一个部分被许多人依赖并且它需要一组复杂的模拟对象(或简单的更多努力来测试它)。可能你不需要测试它。例如,如果 A 使用 B,C 使用 B,而 B 太难测试了。那么,为什么不将 A+B 视为一个,将 C+B 视为花药。在我的示例中,如果 SocketProcessor 太难测试,太难以至于您将花费更多时间测试和维护测试而不是开发它,那么这是不值得的,我将一次测试所有内容。
如果没有看到您的代码(并且事实上我从未开发过 CocooTouch),这将很难说。我也许可以在这里提供很好的评论。对不起:D。
编辑 2
查看您的示例,很明显您正在处理集成问题。假设您已经分别测试播放电影和 UI。为什么需要这么多模拟对象是可以理解的。如果这是您第一次使用这种集成结构(这种并发模式),那么实际上可能需要那些模拟对象,而您对此无能为力。这就是我能说的:-p
希望这可以帮助。
关于unit-testing - 如何在没有这么多模拟的情况下编写测试?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/1595952/