【问题标题】:Writing tests before writing code在写代码之前写测试
【发布时间】:2015-02-10 14:47:17
【问题描述】:

据我了解,TDD 和 BDD 循环类似于:

  1. 从编写测试开始
  2. 看到他们失败了
  3. 编写代码
  4. 通过测试
  5. 重复

问题是在编写任何代码之前如何编写测试?我应该创建某种类骨架或接口吗?还是我误会了什么?

【问题讨论】:

  • 你应该对你将要写什么有一个大概的了解。然后您编写测试,由于类/任何东西不存在而失败,然后您编写使它们通过的最少代码量。由于“测试失败,因为类不存在”很明显,你当然可以跳过它。但是,在纯 TDD 中,您永远不会编写超出通过测试所需的代码,因此请记住这一点。
  • 您可能对计划感到困惑 - TDD 帮助您编写代码,而不是计划事情。因此,当您编写测试时,您应该对程序的结构有一个清晰的概念——类的结构、定义的接口等等。

标签: unit-testing testing tdd bdd


【解决方案1】:

你有它的本质,但我会改变你的描述的一部分。在编写代码之前不要编写测试,而是在编写代码之前编写测试。然后 - 在编写更多测试之前 - 你编写足够的代码来让你的测试通过。当它通过时,您会寻找改进代码的机会,并在保持测试通过的同时进行改进——然后您编写第二个测试。关键是,您在任何给定时间都专注于一点点功能。你想让你的程序做的下一件事是什么?为此编写一个测试,仅此而已。让那个测试通过。清理代码。你想让它做的下一件事是什么?重复直到你满意为止。

问题是,如果你在编写代码之前编写测试,你就没有那个重点。一次只进行一项测试。

【讨论】:

  • 我认为 TDD 的好处之一是了解程序员如何编写使用您的被测接口的代码,以便捕捉接口中的遗漏。例如,如果您正在开发地图,您可能最初不会想到需要提供一种方法来枚举地图中的所有元素,但是在编写测​​试时(以及在您编写任何地图代码之前),您将不可避免地意识到您需要能够枚举元素才能确定地图是否实际包含您要添加到其中的测试元素。
  • 因此,我认为在编写被测代码的单行实现之前,尽可能完整的测试套件将是有益的。
  • 无论如何,@MikeNakis,当您想到您的系统需要的东西时,请记下它。但不要一次编写多个测试。那个笔记——那个笔记列表——可以作为测试的来源,但在你准备好编写导致测试通过的代码之前,它不应该是一个测试。
【解决方案2】:

是的,没错。如果您查看 Michael Hartl 关于 Ruby on Rails 的书(可免费查看 HTML),您将了解他具体是如何做到这一点的。因此,补充一下 lared 所说的内容,假设您的第一份工作是在网页上添加一个新按钮。您的流程如下所示:

  1. 编写测试以直观地查找页面上的按钮。
  2. 验证测试是否失败(不应该存在按钮,因此应该失败)。
  3. 编写代码以在页面上放置按钮。
  4. 验证测试通过。

当您不小心对代码进行了破坏旧测试的操作时,TDD 会保存您的 bacon。例如,您不小心将按钮更改为链接。测试将失败并提醒您注意问题。

【讨论】:

    【解决方案3】:

    如果您使用的是真正的编程语言(您知道,使用编译器等等),那么是的,您当然必须编写类骨架或接口,否则您的测试甚至无法编译。

    如果您使用的是脚本语言,那么您甚至不必编写框架或接口,因为您的测试脚本会愉快地开始运行,并且在遇到第一个不存在的类或方法时会失败。

    【讨论】:

      【解决方案4】:

      问题是在编写任何代码之前如何编写测试?我应该创建某种类骨架或接口吗?还是我误会了什么?

      扩展他在评论中提出的观点:

      然后你编写测试,因为类/任何东西不存在而失败,然后你编写使它们通过的最少量代码

      使用 TDD 要记住的一点是,您正在编写的测试是代码的第一个客户端。因此,我不会担心没有定义类或接口——因为正如他所指出的,只需编写引用不存在的类的代码,你就会在循环中得到你的第一个“红色”——即你的代码不会编译!这是一个完全有效的测试。

      TDD 也可以表示测试驱动设计

      一旦你接受了这个想法,你会发现首先编写测试不再是一个简单的“这段代码是否正确”,而更多的是一个“这段代码是否正确”的指导方针,所以你'你会发现实际上最终生成的产品代码不仅正确,而且结构良好。

      现在展示这个过程的视频会很棒,但我没有,但我会举个例子。请注意,这是一个超级简单的示例,忽略了前期的铅笔和纸张规划/业务的实际需求,这通常是您设计过程背后的驱动力。

      无论如何,假设我们要创建一个简单的 Person 对象,它可以存储一个人的姓名和年龄。我们想通过 TDD 来做这件事,所以我们知道它是正确的。

      所以我们考虑了一分钟,然后编写我们的第一个测试(注意:使用伪 C#/伪测试框架的示例)

      public void GivenANewPerson_TheirNameAndAgeShouldBeAsExpected()
      {
              var sut = new Person();
              Assert.Empty(sut.Name);
              Assert.Zero(sut.Age);
      }
      

      我们马上就有了一个失败的测试,这不会编译,因为 Person 类不存在。因此,您可以使用 IDE 为您自动创建类:

      public class Person
      {
          public int Age {get;set;}
          public string Name {get;set;}
      }
      

      好的,现在您已经通过了第一次测试。但是现在当您查看该类时,您会意识到没有什么可以确保一个人的年龄始终为正(> 0)。让我们断言是这种情况:

      public void GivenANegativeAgeValue_PersonWillRejectIt()
      {
          var sut = new Person();
          Assert.CausesException(sut.Age = -100);
      }
      

      好吧,那个测试失败了,所以让我们修复一下这个类:

      public class Person
      {
          protected int age;
          public int Age 
          {
              get{return age;}
              set{
                      if(value<=0) 
                      {
                          throw new InvalidOperationException("Age must be a positive number");
                      }
                      age=value;
                  }
              }
              public string Name {get;set;}
      }
      

      但是现在你可能会对自己说 - 好吧,既然我知道一个人的年龄永远不可能是 Person,另一个设置Age?如果我忘记在我的代码的一部分中执行此操作怎么办?如果我在我的代码的一部分中创建了一个Person,然后稍后我尝试在另一个模块中将一个负变量分配给Age,该怎么办?当然,Age 必须是 Person 的不变量,所以让我们解决这个问题:

      public class Person
      {
          public Person(int age){
              if (age<=0){
                  throw new InvalidOperationException("Age must be a positive number");
              }
              this.Age = age;
          }   
          public int Age {get;protected set;}
          public string Name {get;set;}
      }
      

      当然,您必须修复您的测试,因为它们将不再编译 - 如果您现在意识到第二个测试是多余的并且可以删除!

      public void GivenANewPerson_TheirNameAndAgeShouldBeAsExpected() { var sut = 新人(42); Assert.Empty(sut.Name); 断言.42(sut.Age); }

      然后您可能会经历与 Name 类似的过程,依此类推。 现在我知道这似乎是一种非常冗长的创建类的方式,但考虑到您基本上是从头开始设计这个类,并内置了对无效状态的防御 - 例如,您将永远不会必须像这样调试代码:

      //A Person instance, 6,000 lines and 3 modules away from where it was instantiated
      john.Age = x; //Crash because x is -42
      

       //A Person instance, reserialised from a message queue in another process
          var someValue = 2015/john.Age; //DivideByZeroException because we forgot to assign john's age 
      

      对我来说,这是 TDD 的主要优点之一,它不仅用作测试工具,而且用作设计工具,让您考虑正在实施的生产代码,并强制执行您需要考虑您创建的类如何最终处于无效、应用程序终止状态,以及如何防范这种情况,并帮助您编写易于使用且不需要其使用者了解它们如何工作的对象,但是而不是他们做什么。

      由于任何值得一试的现代 IDE 都可以让您有机会通过几次击键或鼠标点击来创建缺失的类/接口,我相信这种方法非常值得尝试。

      【讨论】:

        【解决方案5】:

        TDD 和 BDD 是不同的东西,它们共享一个共同的机制。这种共享机制是你在写做某事的东西之前写一些东西来“测试”某事。然后,您使用失败来指导/推动开发。

        (

        您通过思考您要解决的问题来编写测试,并通过假装您有一个可以测试的理想解决方案来充实细节。您编写测试以使用理想的解决方案。这样做会做各种各样的事情,例如:

        1. 发现您的解决方案所需物品的名称
        2. 为您的事物揭开界面,使其易于使用
        3. 在你的东西上经历失败 ...

        BDD 和 TDD 之间的区别在于,BDD 更关注“什么”和“为什么”,而不是“如何”。 BDD 非常关心如何恰当地使用语言来描述事物。 BDD 从更高的抽象层次开始。当您到达细节压倒语言的领域时,TDD 被用作实现细节的工具。

        您可以选择在不同的抽象层次上思考和写下事物的想法是关键。

        您可以通过选择编写所需的“测试”:

        1. 适合您问题的语言
        2. 适当的抽象级别可以简单明了地解释您的问题
        3. 一种适当的机制来调用您的功能。

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2010-09-28
          • 1970-01-01
          • 2014-01-13
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多