关于测试驱动开发的哲学问题

时间:2009-09-19 01:44:00

标签: unit-testing testing tdd

我一直对测试驱动的开发感兴趣,但是当我在实际项目上尝试它时,我永远无法完成它。我尝试时会不断出现一些哲学问题:

  1. 你如何处理大的变化?在测试单个函数(一些参数,结果值,几个副作用)时,TDD是一个明智的选择。但是当你需要彻底检修大的东西时,例如从SAX解析库切换到DOM解析库?当代码处于中间状态时,如何保持测试代码重构周期?一旦你开始进行更改,你将获得一系列失败的测试,直到你完成大修(除非你保持某种类型的mongrel类同时使用DOM和SAX,直到你完成转换,但这很奇怪) 。在这种情况下,小步测试代码重构循环会发生什么?在整个过程中,您将不再进行小型,经过全面测试的步骤。人们必须以某种方式处理这个问题。
  2. 使用模拟测试GUI或数据库代码时,您真正在测试什么?模拟是为了准确地返回您想要的答案而构建的,那么您如何知道您的代码将与真实数据库一起使用?自动化测试对这类事情有什么好处?它在一定程度上提高了信心,但是a)它没有给你完全单元测试应该具有的相同水平的信心,并且b)在某种程度上,你不是简单地验证你的假设是否适用于你的代码而不是您的代码是使用DB还是GUI?
  3. 有人能指出我在大型项目中使用测试驱动开发的优秀案例研究吗?令人沮丧的是,我基本上只能找到单个类的TDD示例。

    谢谢!

8 个答案:

答案 0 :(得分:4)

如何处理大的变化?

尽可能小。

有时重构有很大的表面,但细节很简单。这些可以在很大的步骤中完成。试图打破它们付出太多努力将是浪费。

我认为XML库更改属于此类别。你正在放入XML并获得一些表示。只要您的表示没有改变(从表示状态的图形到事件流),库切换就很容易。

大部分时间重构都不是微不足道的,必须分解。问题是什么时候做更大的步骤和更小的步骤。我的观察是,我很难估计变化的影响。大多数软件都很复杂,你可能认为它们的变化很容易管理,但是整个细则必须再次发挥作用。所以我开始做一些改变。但是,如果它开始变得不可预测,我准备回滚所有内容。我会说这种情况发生在十分之一的重构中。但这个很难。您必须跟踪系统中不符合预期的部分。现在必须在多个较小的问题中分解问题。我一次解决一个问题,并在完成后检查。 (恢复和分裂的多次迭代并不罕见。)

如果您在代码中更改XML解析器和表示,那么这绝对应该是至少两个单独的重构。

模拟测试

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

整个模拟方法可以作为OSI model之类的通信模型。当图层X使用参数x进行调用时,它将使用参数a和b调用图层Z.您的测试指定了此通信协议。

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

尝试以大部分功能松散耦合的方式设计系统。一些功能必须通过模拟测试进行测试(完全解耦的系统是无用的)。

集成测试不是测试系统的选项。它们只应用于测试系统的各个方面,这些方面可能会破坏多个单元的集成。如果您尝试使用集成测试测试系统,则会进入排列赌场。

因此,您的GUI测试策略应该清晰明了。不能单独测试的GUI代码部分应该使用模拟测试进行测试(当按下该按钮时,使用参数y调用服务X)。

数据库淹没了水。您不能模拟数据库,除非您要重新实现您想要支持的每个数据库的行为。但是,这不是单元测试,因为您正在集成外部系统。我已经对这个概念问题采取了和平,并认为DAO和数据库是一个不可分割的单元,可以使用基于状态的测试方法进行测试。 (可悲的是,当它的神圣日与神秘日相比时,这个单位的行为有所不同。它可能在中间突破并告诉你它无法与自己对话。)

答案 1 :(得分:3)

  

使用时测试GUI或数据库代码   嘲笑,你真的在​​测试什么?   模拟的建造是为了准确地返回   回答你想要,所以你怎么知道   您的代码将使用   现实世界的数据库?是什么   自动化测试的好处   之类的事情?它提高了信心   有点,但a)它不会给你   同样的信心水平a   完整的单元测试应该和b)到   在某种程度上,你不是简单的   验证您的假设是否有效   用你的代码而不是你的代码   代码可以与DB或GUI一起使用吗?

这是我的方法:对于数据库访问层(DAL),我不使用mock 进行单元测试。相反,我在真实数据库上运行测试,尽管与生产数据库不同。所以在这个意义上你可以说我不在数据库上运行单元测试。对于NHibernate应用程序,我维护两个具有相同模式的数据库,但不同的数据库类型(ORM使这很简单)。我使用sqlite进行自动化测试,使用真正的MySQL或SQL服务器数据库进行临时测试。

我只用过一次模拟测试DAL;而当我使用强类型数据集作为ORM时(这是一个很大的错误!)。我这样做的方法是让Typemock给我一个完整表格的模拟副本,以便我可以对其执行select *。后来当我回头看时,我希望我永远不会这样做,但那是很久以前的事了,我希望我使用了正确的ORM。

对于GUI,可以对GUI交互进行单元测试。我这样做的方法是使用MVP pattern来分离模型,视图和演示者。实际上,对于这种类型的应用程序,我只在Presenter和Model上进行测试,其中我使用Typemock(或dependency injection)来隔离不同的层,这样我一次只能专注于一层。我不测试视图,但我测试Presenter(大多数交互和错误发生的地方)很多。

答案 2 :(得分:2)

我的2美分......

  1. 如果您的测试因为切换了XML解析器的类型而中断 - 它表示测试脆弱。测试应指定什么而不是。这意味着在这种情况下,测试以某种方式知道您正在使用SAX解析引擎(实现细节);他们不应该这样做。解决这个问题,你应该做大改动。
  2. 当您通过界面从测试中抽象出GUI或模拟时,您确保使用模拟的测试主题(对于实际的协作者来说是双倍的)按预期工作。您可以将代码中的错误与协作者中的错误隔离开来。模拟可以帮助您快速保持测试套件。您还需要一些测试来验证您的真实协作者是否也符合您的真实协作者正确“连线”的界面和测试。

答案 3 :(得分:1)

在处理大型问题方面...... TDD的目的是测试代码的行为以及它与它所依赖的服务的交互方式。如果我们想要使用TDD并且您正在从DOM解析器转移到SAX解析器并且您自己编写了sax解析器,那么您将编写基于已知输入(即XML文档)验证SAX解析器行为的测试。 SAX解析器可能依赖于辅助对象的集合,这些辅助对象实际上最初可能被模拟用于测试SAX解析器的行为。当您准备好为辅助对象编写实现代码时,您可以根据已知输入围绕其预期行为编写测试。在SAX解析器的示例中,您将编写单独的类来实现此行为,以便不会干扰依赖于DOM解析器的现有代码。实际上,您可以创建一个IXMLParser接口,DOM解析器和SAX解析器实现,以便您可以随意切换它们。

就使用模拟或存根而言,使用Mock或Stub的原因是你对测试Mock或Stub的内部工作方式不感兴趣,但是你有兴趣测试什么是内部工作原理取决于模拟或存根,这是你从单元的角度真正测试的。如果您对编写集成测试感兴趣,那么您应该编写集成测试而不是单元测试。我发现以TDD方式编写代码对于帮助我根据我被要求提供的行为定义代码的结构和组织非常有用。

我不熟悉任何案例研究,但我相信他们在那里。

答案 4 :(得分:1)

  

使用时测试GUI或数据库代码   嘲笑,你真正在测试什么?

我通常会尝试将业务逻辑,显示逻辑和数据库访问分开。我的大多数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的范围,因为设置它们通常非常耗时。

答案 5 :(得分:1)

  1. 您如何处理大的变化?

    • 步骤一步。我已经开展了很多非平凡的计划,并且总是能够将事情分解为微小的变化(需要数小时甚至数天)。例如,重写一个30Mpv的网站分解为一次做一个页面 - 这是从一种语言转移到另一种语言,在我们去的时候编写(小)测试,保持网站频繁部署。我们将GUI Web应用程序转换为无头后端服务器的另一个项目。这涉及到一个月或两个工作的许多小步骤,然后最终丢弃大部分Web代码。但是我们能够保持所有测试的正常运行。我们这样做并不是因为我们试图证明某些东西,而是因为这是重用代码和测试的最佳方式。

    • 更大范围的测试可以帮助实现更大的步骤。例如,您的SAX-> DOM示例将具有可验证最终行为的高级集成测试。但是,当我做了类似的事情时,我围绕不同类型的节点处理编写了更小的行为测试,并且可以逐个进行转换。

  2. 使用模拟测试GUI或数据库代码时,您还在测试什么?

    • 你总是需要确保你正在编写有价值的测试。这可能很难。即使您正在考虑,也很容易编写一些非常冗余的测试。
    • 当您尝试测试数据库查询时,模拟没有意义。当你试图“模拟”低于你正在测试的层时,它们很有用......所以它们在控制器测试中是有用的,你将模拟服务层的行为 - 你将独立测试。 为了测试数据库查询,您需要使用适当的fixture来加载数据库,以测试您要执行的操作。这可以通过固定装置或仔细的测试设置代码来完成。这需要一些思考才能做到正确,因此拥有一套精心设计的夹具数据是很好的,这些数据将使您能够编写良好的数据库查询测试,涵盖尽可能多的重要案例。

    • 是的,您正在使用模拟验证您的假设 - 但您还必须单独测试这些假设。替代方案 - 一起测试它们 - 很好,但更脆弱。这意味着测试正在测试更多代码,因此可以更容易地破解。

答案 6 :(得分:0)

至于数据库角度,正如Ngu Soon Hui所提到的那样,你应该(IMHO)使用类似DBUnit的东西,它将以已知的配置设置数据库(这样你就可以测试预期的结果)但是它正在使用真实应用程序将使用的真实数据库。

对于大的更改,我建议创建一个分支,并允许测试失败。这将为您提供一个需要更改的区域的TODO列表,可以说这是TDD真正发挥作用的地方,甚至超过了小型的隔离功能。

答案 7 :(得分:0)

处理大量更改

根据我的经验,这些相对较少。当它们发生时,更新测试是一个小麻烦。诀窍是为测试选择正确的粒度。如果您测试公共接口,更新将很快进行。如果您测试私有实现代码,从SAX更改为DOM解析器将会耗费大量时间,您会感觉到dom。 ; - )

测试GUI代码

总的来说,我没有。我保持UI层尽可能薄。我们的想法是测试可能会破坏的东西。

测试数据库代码

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

很有可能滥用模拟和存根,编写除了测试模拟对象之外什么都不做的测试。编写好的测试需要很多经验;我还在学习。

我的建议是继续练习TDD,也许最初是在较小的项目上。尽可能多地阅读,与其他从业者交谈,并使用适合您的方法。

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

编辑:坦率地说,在很多情况下我无法解耦数据访问代码,最终使用测试卡数据库。尽管它们更慢且更脆弱,但即使是像这样的集成测试也证明是有价值的。正如我所说,我还在学习。