如何在没有这么多嘲笑的情况下编写测试?

时间:2009-10-20 16:52:35

标签: unit-testing tdd mocking

我是正确的测试驱动设计或行为驱动设计的忠实拥护者,我喜欢编写测试。但是,我一直把自己编码到一个角落里,我需要在一个特定的测试用例中使用3-5个模拟进行单个类。无论我从哪个方向开始,自上而下或自下而上,我最终都需要一个至少需要三个来自最高抽象级别的合作者的设计。

有人可以就如何避免这个陷阱给出好的建议吗?

这是一个典型的场景。我设计了一个Widget,它从给定的文本值生成一个Midget。在我深入了解细节之前,它始终非常简单。我的Widget必须与几个难以测试的东西进行交互,比如文件系统,数据库和网络。

所以,不是将所有这些设计到我的Widget中,而是制作一个Bridget合作者。 Bridget负责处理复杂性,数据库和网络的一半,使我能够专注于另一半的多媒体演示。那么,我制作了一个执行多媒体作品的Gidget。整个事情需要在后台发生,所以现在我包含了一个Thridget来实现这一点。完成所有操作后,我最终得到了一个Widget,它可以将工作交给Thridget,后者通过Bridget进行操作,将结果发送给Gidget。

因为我在CocoaTouch中工作并试图避免使用模拟对象,所以我使用自分流模式,其中协作者的抽象成为我的测试采用的协议。有3个以上的合作者,我的测试气球变得太复杂了。即使使用像OCMock模拟对象这样的东西,也会给我一个复杂的顺序,我宁愿避免。我试着把我的大脑缠绕在一个菊花链的合作者身上(一个委托给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);

现在上面显然处理网络延迟,所以它必须是Threaded。这引入了一个Thread抽象,使我们脱离了线程单元测试的实践。我们现在有

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按钮点击事件联系在一起。因此,最外面的测试反映了一个Sprint任务,该任务在给出“Play Movie”按钮的情况下,当点击它时,电影将播放。 的 修改 让我们讨论。

4 个答案:

答案 0 :(得分:7)

拥有许多模拟对象表明:

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创建模拟,这可能需要在它们后面有相当复杂的类来支持。

但是如果仔细观察,从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很难测试,那么你将花费更多的时间来测试和维护测试而不是开发测试,那么它是不值得的,我将立即测试整个事情。 / p>

没有看到你的代码(以及我永远不会开发CocaoTouch的事实),很难说。我也许可以在这里提供好的评论。对不起:D。

编辑2 看看你的例子,你很清楚你正在处理集成问题。假设您已经分别测试了播放电影和UI。你需要这么多模拟对象是可以理解的。如果这是您第一次使用这种集成结构(此并发模式),那么实际上可能需要这些模拟对象,而您无能为力。这就是我所能说的:-p

希望这有帮助。

答案 1 :(得分:0)

我做了一些相当完整的测试,但它是自动集成测试而不是单元测试,所以我没有模拟(除了用户:我模拟最终用户,模拟用户输入事件并测试/断言输出到的任何内容)用户):Should one test internal implementation, or only test public behaviour?


我正在寻找的是使用TDD的最佳做法。

Wikipedia describes TDD as,

  

一种软件开发技术   依赖于重复   开发周期短:首先是   开发人员编写失败的自动化   定义所需的测试用例   那么改进或新功能   生成代码来传递该测试   最后重构新代码   可接受的标准。

然后继续规定:

  1. 添加测试
  2. 运行所有测试并查看新测试是否失败
  3. 写一些代码
  4. 运行自动化测试并看到它们成功
  5. 重构代码
  6. 我做了第一个,即“非常短的开发周期”,我的情况就是我在写完之后进行测试。

    我在编写之后测试的原因是我不需要“编写”任何测试,甚至是集成测试。

    我的周期类似于:

    1. 重新运行所有自动化集成测试(从干净的平板开始)
    2. 实施新功能(必要时重构现有代码以支持新功能)
    3. 重新运行所有自动化集成测试(回归测试以确保新开发没有破坏现有功能)
    4. 测试新功能:

      一个。最终用户(我)通过用户界面进行用户输入,旨在实施新功能

      湾最终用户(我)检查相应的程序输出,以验证输出对于给定的输入是否正确

    5. 当我在步骤4中进行测试时,测试环境将用户输入和程序输出捕获到数据文件中;测试环境可以在将来重播这样的测试(重新创建用户输入,并断言相应的输出是否与先前捕获的预期输出相同)。因此,在步骤4中运行/创建的测试用例被添加到所有自动化测试的套件中。

    6. 我认为这给了我TDD的好处:

      • 测试与开发相结合:我在编码后立即进行测试,而不是在编码之前进行测试,但无论如何,新代码在签入之前都要经过测试;从来没有未经测试的代码。

      • 我有自动化测试套件,用于回归测试

      我避免了一些成本/劣势:

      • 编写测试(相反,我使用UI创建新测试,更快更容易,更接近原始要求)

      • 创建模拟(单元测试所需)

      • 在重构内部实现时编辑测试(因为测试仅依赖于公共API,而不依赖于内部实现细节)。

答案 2 :(得分:0)

我的解决方案(不是CocoaTouch)是继续模拟对象,而是将refactory mock设置为常用的测试方法。这样可以降低测试本身的复杂性,同时保留模拟基础架构以单独测试我的类。

答案 3 :(得分:0)

为了摆脱过度的嘲弄你可以遵循Test Pyramid,这表明有很多单位+组件测试和较少数量的慢速&脆弱的系统测试。它归结为几个简单的规则:

  • 以尽可能低的级别编写测试,不需要模拟。如果您可以编写单元测试(例如解析字符串),则编写它。但是如果你想检查上层是否调用了解析,那么这需要初始化更多的东西。
  • 模拟外部系统。您的系统需要是一个独立的,独立的部分。依赖外部应用程序(会有自己的错误)会使测试变得复杂。编写模拟/存根更容易。
  • 之后有几个测试用真正的集成检查你的应用程序。

凭借这种心态,你几乎可以消除所有嘲弄。