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

关于测试驱动开发的庸俗问题

  •  18
  • Bill  · 技术社区  · 17 年前

    我一直被测试驱动开发所吸引,但是当我在真正的项目上尝试的时候,我永远无法坚持下去。当我尝试的时候,我有几个哲学问题不断出现:

    1. 你如何处理大的变化?当涉及到测试单一功能(一些参数,一个结果值,很少的副作用),TDD是一个无需考虑。但是,当您需要彻底检查一些大型的东西时,比如从sax解析库切换到dom解析库,又该怎么办呢?当代码处于中间状态时,如何保持测试代码重构周期?一旦您开始进行更改,您将得到大量失败的测试,直到您完全完成彻底的检查(除非您在完成转换之前维护某种同时使用dom和sax的混血儿类,但这非常奇怪)。在这种情况下,小步骤测试代码重构周期会发生什么变化?在这整个过程中,你将不再是在小的,充分测试的步骤移动。人们一定有办法解决这个问题。
    2. 当使用mock测试gui或数据库代码时,您真正要测试的是什么?mock是为返回您想要的答案而构建的,那么您如何知道您的代码将与真实的数据库一起工作呢?这种东西的自动化测试有什么好处?它在一定程度上提高了可信度,但是a)它并没有给你一个完整的单元测试应该给你的可信度,b)在一定程度上,你不是简单地验证你的假设是否适用于你的代码,而不是你的代码是否适用于db或gui吗?

    有人能指出我在大型项目中使用测试驱动开发的好案例吗?令人沮丧的是,我基本上只能为单个类找到TDD示例。

    谢谢!

    8 回复  |  直到 17 年前
        1
  •  4
  •   Matthew Groves    17 年前

    你如何处理大的变化?

    根据需要尽可能小。

    有时重构有一个很大的表面,但在细节上是微不足道的。这些可以在相当大的步骤中完成。花太多的精力去打破它们将是浪费。

    我认为xml库的更改属于这一类。你把XML放进去,然后得到一些表示。只要表示没有改变(从表示状态的图变为事件流),库切换就很容易。

    大多数情况下,重构并不简单,必须被分解。问题是什么时候该迈出更大的一步,什么时候该迈出更小的一步。我的观察是,我不善于估计变化的影响。大多数软件都很复杂,你可能会认为它们的更改很容易管理,但接下来就要重新开始工作了。所以我从一开始就做了一些改变。但如果事情开始变得不可预测,我会把一切都退回去。我认为这种情况发生在十分之一的重构中。但这次会很难。你必须找出系统中不符合你期望的部分。现在必须把问题分成多个小问题。我一次解决一个问题,完成后再登记。(还原和拆分的多次迭代并不少见。)

    如果在代码中更改XML解析器和表示,那么这肯定至少是两个独立的重构。

    模拟测试

    您正在使用模拟对象测试对象/层之间的通信协议。

    整个模拟方法可以看作是一种通信模型,如 OSI model . 当x层被参数x调用时,它将用参数a和b调用z层。您的测试指定了这个通信协议。

    与模拟测试一样有用,尽可能少的测试功能。最好的选择是基于状态的测试:设置fixture、调用被测系统、检查被测系统的状态和纯功能(如在功能编程中)测试:用x调用返回a。

    尝试以一种大多数功能都是松散耦合的方式设计系统。有些功能必须通过模拟测试来测试(完全分离的系统是无用的)。

    集成测试不是测试系统的选项。它们只应用于测试系统的各个方面,这些系统可以随着多个单元的集成而中断。如果您尝试用集成测试测试您的系统,您将进入排列赌场。

    所以你的gui测试策略应该很清楚。不能单独测试的gui代码部分应该使用模拟测试进行测试(当按下此按钮时,用参数y调用服务x)。

    数据库把水弄脏了。不能模拟数据库,除非要重新实现要支持的每个数据库的行为。但这不是一个单元测试,因为您正在集成一个外部系统。我用这个概念性的问题解决了问题,把DAO和数据库看作一个不可分割的单元,可以用基于状态的测试方法进行测试。(不幸的是,这个单元在甲骨文日的表现与mysql日不同。它可能会在中间裂开,告诉你它不能自言自语。)

        2
  •  3
  •   Graviton    17 年前

    当使用 Mocks,你到底在测试什么? 模拟是为了返回 你想要的答案,你怎么知道 您的代码将与 真实世界数据库?什么是 自动化测试的好处 什么事?它提高了信心 有点,但是a)它没有给你 同样的自信程度 完整单元测试应该,和b)到 在某种程度上,你不是简单的 验证您的假设是否有效 用你的代码而不是你的 代码与数据库或图形用户界面一起工作?

    这是我的方法:对于数据库访问层(dal),我 不要使用mock 为了我的单元测试。相反,我在一个真实的数据库上运行测试,尽管它与生产数据库不同。所以从这个意义上说,我不在数据库上运行单元测试。对于nhibernate应用程序,我使用相同的模式维护两个数据库,但数据库类型不同( ORM 这很简单)。我用 sqlite 对于我的自动化测试,和一个真正的my sql或sql server数据库进行即席测试。

    只有一次我用模拟来测试DAL,那时我使用强类型数据集作为ORM(一个大错误!)我这样做的目的是 Typemock 给我一本完整表格的仿制品,这样我就可以表演了。 select * 关于它。后来我回想起来,我希望我永远不会这样做,但那是很久以前的事了,我希望我用一个合适的ORM。

    至于图形用户界面, 这是可能的 对gui交互进行单元测试。我这样做的方式是使用 MVP pattern 将模型、视图和演示者分开。实际上,对于这种类型的应用程序,我只在演示者和模型上进行测试,在模型中使用typemock(或者 dependency injection 隔离不同的层,这样我只能集中一层。我不测试视野,但我 做 测试演示者(大多数交互和错误都发生在这里)很多。

        3
  •  2
  •   Gishu    17 年前

    我的2美分…

    1. 如果您的测试由于切换了XML解析器的类型而中断,则表明测试是 脆弱的 . 测试应该指定 什么而不是如何 . 这意味着在本例中,测试不知何故知道您使用的是sax解析引擎(一个实现细节);但它们不应该这样做。解决这个问题,你应该更好地处理大的变化。
    2. 当您通过接口从测试中抽象出gui或mock时,您要确保使用mock的测试主题(对于实际的协作者来说是double)按预期工作。您可以将代码中的错误与协作者中的错误隔离开来。mock可以帮助您保持测试套件的快速性。你还需要验证你的真正的合作者是否也符合接口的测试,以及测试你的真正的合作者是否正确连接。
        4
  •  1
  •   Michael Mann    17 年前

    在处理大的案件方面…tdd的目的是测试代码的行为,以及代码如何与它所依赖的服务交互。如果您想使用tdd,并且从dom解析器迁移到sax解析器,并且您自己编写sax解析器,那么您可以编写测试,根据已知的输入(即xml文档)验证sax解析器的行为。sax解析器可能依赖于helper对象的集合,这些对象实际上可以在最初被模拟出来,以测试sax解析器的行为。当您准备好为helper对象编写实现代码时,可以根据已知的输入围绕其预期行为编写测试。在sax解析器的示例中,您将编写单独的类来实现此行为,以免干扰依赖于dom解析器的现有代码。实际上,您可以创建一个IXMLParser接口,由DOM解析器和SAX解析器实现,这样您就可以随意将它们切换出去。

    就使用mock或stub而言,使用mock或stub的原因是您对测试mock或stub的内部工作不感兴趣,但是您对测试依赖于mock或存根的内部工作感兴趣,而这正是您从单元角度真正测试的内容。如果您对编写集成测试感兴趣,那么您应该编写集成测试,而不是单元测试。我发现以tdd的方式编写代码对于帮助我定义代码的结构和组织(围绕我被要求提供的行为)非常有用。

    我不熟悉任何现成的案例研究,但我确信它们都在那里。

        5
  •  1
  •   Bugmaster    17 年前

    当使用 Mocks,你到底在测试什么?

    我通常尝试分离我的业务逻辑、显示逻辑和数据库访问。我的大多数gui单元测试都涉及业务逻辑。下面是一个伪代码示例:

    // Production code in class UserFormController:
    
    void changeUserNameButtonClicked() {
      String newName = nameTextBox.getText();
      if (StringUtils.isEmpty(newName)) {
        errorBox.showError("User name may not be empty !");
      } else {
        User user = engine.getCurrentUser();
        user.name = newName;
        engine.saveUser(user);
      }
    }
    
    // Test code in UserFormControllerTest:
    
    void testValidUserNameChange() {
      nameTextBox = createMock(TextBox.class);
      expect(nameTextBox.getText()).andReturn("fred");
      engine = createMock(Engine.class);
      User user = createMock(user);
      user.setName("fred");
      expectLastCall();
      expect(engine.getCurrentUser()).andReturn(user);
      engine.saveUser(user);
      expectLastCall();
      replay(user, engine, nameTextBox);
    
      UserFormController controller = new UserFormController();
      controller.setNameTextBox(nameTextBox);
      controller.setEngine(engine);
      controller.changeUserNameButtonClicked();  
    
      verify(user, engine, nameTextBox);
    }
    
    void testEmptyUserNameChange() {
      nameTextBox = createMock(TextBox.class);
      errorBox = createMock(ErrorBox.class);
      expect(nameTextBox.getText()).andReturn("");
      errorBox.showError("User name may not be empty !");
      expectLastCall();
      replay(nameTextBox, errorBox);
    
      UserFormController controller = new UserFormController();
      controller.setNameTextBox(nameTextBox);
      controller.setErrorBox(errorBox);
      controller.changeUserNameButtonClicked();  
    
      verify(nameTextBox, errorBox);
    }
    

    这确保了,不管我的数据库和gui代码有多糟糕,至少控制用户名更改的逻辑能够正常工作。如果将GUI代码组织成一组单独的控件(或小部件或窗体元素或它们在GUI框架中调用的任何内容),则可以以类似的方式对它们进行测试。

    但最终,就像你所说的,这些单元测试并不能给你完整的描述。要做到这一点,您需要按照其他人的建议:创建一个具有“黄金集”数据的真实数据库,并对其运行集成/功能测试。但是,imo,这种测试不在tdd的范围内,因为设置它们通常非常耗时。

        6
  •  1
  •   ndp    17 年前
    1. 你如何处理大的变化?

      • 一步一步。我已经做了很多非琐碎的程序,并且总是能够把事情分解成小的变化(需要几个小时甚至几天)。例如,改写一个30MPV网站一次只做一页——这是从一种语言转移到另一种语言,写(小)测试,因为我们去,保持网站的频繁部署。另一个项目我们将一个gui web应用程序转换为一个无头后端服务器。这需要在一两个月的工作中进行许多小步骤,然后最终丢弃大部分web代码。但我们能让所有的测试继续进行。我们这样做不是因为我们试图证明什么,而是因为它是重新使用代码和测试的最佳方式。

      • 更大的步骤可以通过范围更广的测试来辅助。例如,sax->dom示例将有一个高级集成测试来验证最终行为。不过,当我做类似的事情时,我围绕不同类型的节点处理编写了更小的行为测试,并且可以逐个转换这些测试。

    2. 当使用mock测试gui或数据库代码时,您真正要测试的是什么?

      • 你总是需要确保你写的是有价值的测试。这可能很难。即使你在想,写一些非常多余的测试也很容易。
      • 当您尝试测试数据库查询时,模拟没有意义。当你试图“模拟”你正在测试的下面的一层时,它们很有用…因此,它们在控制器测试中非常有用,您可以模拟服务层的行为——您可以独立测试服务层的行为。 为了测试数据库查询,您需要使用适当的fixture加载数据库,以测试您正在尝试做的事情。这可以用固定装置或仔细的测试设置代码来完成。这需要一些考虑,所以最好有一组设计良好的fixture数据,使您能够编写好的数据库查询测试,尽可能多地覆盖重要的情况。

      • 是的,您正在用mock验证您的假设——但是您还必须单独测试这些假设。另一种选择——将它们全部测试在一起——很好,但更脆弱。这意味着一个测试是测试更多的代码,因此可以更容易地破解。

        7
  •  0
  •   pkaeding    17 年前

    至于数据库角度,正如ngu soon hui提到的,您应该(imho)使用 DBUnit 它将在已知的配置中设置数据库(因此,您可以测试预期的结果),但它使用的是实际应用程序所使用的真实数据库。

    对于较大的更改,我建议创建一个分支,并允许测试失败。这将为您提供一个需要更改的区域的todo列表,可以说这是tdd真正的亮点,甚至比使用小的、独立的函数更重要。

        8
  •  0
  •   TrueWill    17 年前

    处理重大变化

    以我的经验来看,这是比较少见的。当它们发生时,更新测试只是一个小麻烦。诀窍是为测试选择正确的粒度。如果测试公共接口,更新将很快进行。如果您测试私有实现代码,那么从sax更改为dom解析器将在很大程度上sax,您将感受到dom。;-)

    测试gui代码

    一般来说,我不会。我尽量保持我的ui层薄。这个想法是为了测试什么可能会坏。

    测试数据库代码

    如果可能的话,我更喜欢将数据访问代码放在接口后面,并在测试业务逻辑时模拟出来。正如其他人提到的,在某些时候,您可能希望对dal运行集成测试,以确保它在已知状态下对测试数据库工作。您可能需要对整个系统进行其他集成测试;拥有不同类型的测试层是一件好事。TDD主要是关于设计的,并不排除集成或验收测试的需要。

    很有可能滥用mock和stub,编写只测试mock对象的测试。写好测试需要很多经验,我还在学习。

    我的建议是继续练习tdd,也许最初是在较小的项目上。尽可能多地阅读它,与其他从业者交谈,并使用对你有用的东西。

    持续集成确实有助于测试,因为它可以确保测试运行并使中断的测试可见。强烈推荐。

    编辑:坦率地说,在很多情况下,我在分离数据访问代码时遇到了困难,最终使用的是test-deck数据库。即使像这样的集成测试也被证明是有价值的,尽管它们速度慢而且更脆弱。正如我所说,我还在学习。

    推荐文章