代码之家  ›  专栏  ›  技术社区  ›  Cliff

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

  •  20
  • Cliff  · 技术社区  · 15 年前

    我大力提倡正确的测试驱动设计或行为驱动设计,我喜欢编写测试。然而,我一直把自己编码到一个角落,我需要在一个特定的测试用例中为单个类使用3-5个模拟。无论我从哪种方式开始,自上而下还是自下而上,我最终的设计都需要来自最高抽象级别的至少三个协作者。

    这是一个典型的场景。我设计了一个小部件,它根据给定的文本值生成一个侏儒。在我进入细节之前,它总是非常简单。我的Widget必须与一些难以测试的东西交互,比如文件系统、数据库和网络。

    因为我在CocoaTouch中工作,并且试图避免模拟对象,所以我使用了自分流模式,在这种模式中,协作者上的抽象成为我的测试采用的协议。有了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按钮单击事件。因此,outter most测试反映了一个Sprint任务,即给定一个“播放电影”按钮,当它被单击时,电影将播放。 编辑 让我们讨论一下。

    4 回复  |  直到 12 年前
        1
  •  8
  •   NawaMan    15 年前

    拥有许多模拟对象表明:

    1) 你有太多的依赖关系。 重新审视你的代码,并尝试进一步分解它。特别是,尝试将数据转换和处理分开。

    因为我对你所处的环境没有经验。因此,让我以自己的经验为例。

    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很难测试,太难了以至于你会花更多的时间来测试和维护测试,而不是开发它,那么这是不值得的,我将立即测试所有东西。

    如果没有看到你的代码(事实上,我从来没有开发过cocotouch),那就很难说了。我可能会在这里提供很好的评论。对不起,D。

    希望这有帮助。

        2
  •  0
  •   tvanfosson    15 年前

    我的解决方案(不是CocoaTouch)是继续模拟对象,而是将模拟重构为一种通用的测试方法。这降低了测试本身的复杂性,同时保留了模拟基础结构,以单独测试我的类。

        3
  •  0
  •   Community CDub    7 年前

    Should one test internal implementation, or only test public behaviour?


    我要寻找的是使用TDD的最佳实践。

    Wikipedia describes TDD 作为,

    依赖于一个非常复杂的过程的重复 开发周期短:首先是 定义所需测试的测试用例 生成代码以通过该测试,并 最后将新代码重构为

    然后,它继续规定:

    1. 添加一个测试
    2. 写一些代码
    3. 重构代码

    我做了其中的第一个,即“非常短的开发周期”,不同之处在于我在编写之后进行了测试。

    我之所以在编写之后进行测试,是因为我根本不需要“编写”任何测试,即使是集成测试。

    我的周期是这样的:

    1. 重新运行所有自动化集成测试(从头开始)
    2. 重新运行所有自动化集成测试(回归测试以确保新开发没有破坏现有功能)
    3. 测试新功能:

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

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

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

    我避免了一些成本/缺点:

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

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

        4
  •  0
  •   Stanislav Bashkyrtsev    8 年前

    为了摆脱过度的嘲弄,你可以按照 Test Pyramid

    • 以不需要模拟的最低级别编写测试。如果您可以编写单元测试(例如,解析字符串),那么就编写它。但是如果您想检查上层是否调用了解析,那么这将需要初始化更多的内容。
    • 模拟外部 系统 . 您的系统需要是一个自包含、独立的部分。依赖外部应用程序(它们会有自己的bug)会使测试复杂化很多。编写模拟/存根要容易得多。
    • 之后,进行两次测试,用真正的集成检查你的应用程序。

    有了这种心态,你几乎消除了所有的嘲弄。