如何使用测试驱动的开发方法验证布尔方法?

时间:2014-04-10 15:35:12

标签: java unit-testing tdd

我正在尝试学习Java,同时实现测试驱动开发(TDD)方法。我在面向对象编程概念方面有一些经验来理解Java而不是TDD。我写了一个非常简单的Java程序和单元测试,但在解决问题时我遇到了一些问题。我认为有人在这里可能能够澄清我的问题。我列出了问题,我的最终单元测试类,类实现逻辑,我用来解决问题的方法以及我的问题。

问题:

我采用了SleepIn网站上列出的第一个问题CodingBat来尝试实施简单的单元测试。这是网站上所述的问题。

  

如果是工作日,则参数weekday为true,如果我们正在度假,则参数vacation为true。如果不是工作日或者我们正在休假,我们会睡觉。如果我们入睡,则返回true。

单元测试:

我编写了单元测试类并创建了四种方法来测试可用于上述问题的可能输入的结果。

package com.codingbat.practice.unit;

import static org.junit.Assert.*;
import org.junit.Test;
import com.codingbat.practice.SleepIn;

public class SleepInTest {

    @Test
    public void weekend_and_not_vacation() {
        SleepIn sleepIn = new SleepIn();
        sleepIn.setWeekday(false);
        sleepIn.setVacation(false);
        assertTrue(sleepIn.allow());
    }

    @Test
    public void weekend_and_vacation() {
        SleepIn sleepIn = new SleepIn();
        sleepIn.setWeekday(false);
        sleepIn.setVacation(true);
        assertTrue(sleepIn.allow());
    }

    @Test
    public void weekday_and_not_vacation() {
        SleepIn sleepIn = new SleepIn();
        sleepIn.setWeekday(true);
        sleepIn.setVacation(false);
        assertFalse(sleepIn.allow());
    }

    @Test
    public void weekday_and_vacation() {
        SleepIn sleepIn = new SleepIn();
        sleepIn.setWeekday(true);
        sleepIn.setVacation(true);
        assertTrue(sleepIn.allow());
    }
}

类实现:

这是解决上述问题的类实现。

package com.codingbat.practice;

public class SleepIn {
    boolean weekday;
    boolean vacation;

    public SleepIn() {
    }

    public void setWeekday(boolean weekday) {
        this.weekday = weekday;
    }

    public void setVacation(boolean vacation) {
        this.vacation = vacation;
    }

    public boolean allow() {
        return (!this.weekday || this.vacation);
    }
}

我采取的方法:

  1. 写了方法SleepIn.allow(),只返回 false

  2. 实现了四个单元测试并传递了适当的参数,并根据我认为应该是类实现的结果使用了必要的断言方法。

  3. 执行测试用例。其中3人失败,1人成功。

  4. 测试weekday_and_not_vacation()成功,因为它需要 false 并且实现当前仅返回false。

  5. 在方法SleepIn.allow()中实施必要的逻辑来解决问题。

  6. 再次执行测试。所有4项测试都通过了。

  7. 在实现逻辑中用||替换&&以破解解决方案并再次运行测试以验证至少有一个测试失败。 4次测试中有2次失败。再次将正确的逻辑重新放入方法中,以验证所有测试是否成功。

  8. 问题:

    1. 我对TDD的理解是在编写实现逻辑之前使测试失败。在这种情况下,该方法返回布尔值。因此,我只能返回 true false 。我无法让所有测试都无法开始。推荐的TDD方法是否与我为解决问题所采取的步骤相匹配?

    2. 在各种网站上阅读TDD,我推断每个单元测试应该是相互独立的。基于此,我在每个单元测试中创建了SleepIn的实例,以将所有测试逻辑限制在每个单元测试方法中。那通常是如何实施单元测试的呢?

    3. 在实现类中,我可以使用两个参数创建构造函数来实例化实例变量。但是,我创建了 setters ,因为调用方法名称来传递值似乎更有意义。我知道如果没有调用适当的setter,这种情况下的实例变量将包含 false 。 Java或一般面向对象编程实现中推荐的方法是什么?我没有实现 getters ,因为我觉得不需要这个问题。

    4. 我认为在编写单元测试时必须知道任何给定输入的结果是什么,以便我们可以使用适当的断言。在这种情况下,输入是两个布尔,我知道输出是什么,因为问题非常简单。因此,我相应地选择了相应的assertTrueassertFalse。这是单元测试应该如何编写的?当逻辑变得复杂时,例如在现实世界的应用程序中,它会如何扩展?如果它变得复杂,是否意味着问题应该被分解并保持尽可能小,以便我们可以轻松地进行单元测试?

2 个答案:

答案 0 :(得分:1)

在标准TDD中,您应该一次只编写一个测试,然后通过该测试。从正常情况开始,比如weekday_and_not_vacation。看到它失败了,因为你的回归是真的。返回false,看着它通过。现在采取not_weekday_and_not_vacation。看到它失败,因为你返回false。放入检查工作日的逻辑;现在两个测试都通过现在是工作日_和_vacation,泡沫,冲洗,重复。

  1. 因为你先写完了所有的测试,所以你最后通过了一些测试。当你一次编写并通过一次测试时,这种可能性就会降低。
  2. 为每个测试创建一个新的SleepIn实例是非常合理的。没有必要,但合理且绝不违反标准的TDD实践。
  3. 制定者或参数很好;在你使用班级的背景下最有意义的事情。
  4. 是的,您应该始终能够提前知道预期结果是什么。如果您的问题太复杂,请将其分解,直到您可以说出您的测试应该执行的下一部分。这是设计在测试驱动设计中发挥作用的关键部分。

答案 1 :(得分:1)

首先,要单独解决您的问题:

  

1.我对TDD的理解是在编写实现逻辑之前使测试失败。在这种情况下,该方法返回一个布尔值。因此,我只能返回true或false。我无法让所有测试都无法开始。推荐的TDD方法是否与我为解决问题所采取的步骤相匹配?

TDD requires humbly suggests您一次编写一个测试,实现传递每个测试所需的代码,然后继续下一个。

这不仅减少了在任何给定时间写入的代码量,它实际上也影响了设计选择。听听你的测试。

  

2.在各种网站上阅读TDD,我推断每个单元测试应该是相互独立的。基于此,我在每个单元测试中创建了一个SleepIn实例,以将所有测试逻辑限制在每个单元测试方法中。那通常是如何实施单元测试的呢?

这是在单元测试中处理SUT的常用方法。主要是在测试之间最小化/避免共享资源(例如,类的实例)。如果测试之间共享状态,则编写它们会变得更加困难。 听取您的测试。

  

3.在实现类中,我可以使用两个参数创建构造函数来实例化实例变量。但是,我创建了setter,因为调用方法名称来传递值似乎更有意义。我知道如果没有调用适当的setter,这种情况下的实例变量将包含false。 Java或一般面向对象编程实现中推荐的方法是什么?我没有实施吸气剂,因为我觉得不需要这个问题。

这没有一般的经验法则;如果某些事情需要通过设定者来配置参数,那么就有一个现有的驱动力。但是,TDD要求您只编写尽可能多的代码来通过测试。通过此测试是否需要制定者?如您所述,还有其他方法可以将这些参数输入SUT。 聆听您的测试。

  

4.我认为在编写单元测试时必须知道任何给定输入的结果是什么,以便我们可以使用适当的断言。在这种情况下,输入是两个布尔值,我知道输出是什么,因为问题非常简单。所以,我相应地选择了相应的assertTrue或assertFalse。这是单元测试应该如何编写的?当逻辑变得复杂时,例如在现实世界的应用程序中,它会如何扩展?如果它变得复杂,是否意味着问题应该被分解并保持尽可能小,以便我们可以轻松地进行单元测试?

确保您的测试具有确定性是绝对必要的。 (如果他们不是,你怎么知道他们是可靠的?)测试需要完全控制SUT操作的上下文。

如果SUT需要生成一个随机数,那么测试应supply a dummy object to the SUT生成一个不那么随机的数字。

如果SUT需要询问系统时钟当前时间,测试应提供一个有效冻结的物体。

如果测试开始变得太复杂而无法维护,那就试着告诉你一些事情。

Listen to your tests.

  

... [W]母鸡代码难以测试,最可能的原因是我们的设计需要改进。使代码难以测试的相同结构将使其在将来难以改变。当未来到来时,变化将更加困难,因为我们在编写代码时已经忘记了我们的想法。

关于TDD的事情是,它实际上并不专注于测试,而是设计系统是健壮的和模块化的。测试恰好是验证这种设计的最简单方法之一(即,除非您的系统是健壮且模块化的,否则测试很难编写)。

通过要求我们在编写任何相应代码之前编写测试,TDD基本上迫使我们codify our requirements。也就是说,单元测试是系统要求的代码 - 实施例。

考虑到这一点,从需求的角度编写测试会很有帮助。 ("是否需要这个?如果不是,那为什么我要编写所有这些代码?")

例如,您的CodingBat问题的网页似乎暗示要求存在具有以下签名的方法:

public boolean sleepIn(boolean weekday, boolean vacation) {
    // TODO
}

从这个前提出发,我们可以更好地回答你的问题#3 - 我们可以将它们传递给函数本身,而不是在setter中或者在类的构造函数中传递参数。

现在让我们考虑问题的规定要求:

  1. 如果是周末,请允许我们入睡
  2. 如果我们在度假,请允许我们入睡
  3. 我们应该能够将每个要求映射到单个单元测试。想象一下,我们在单元测试can_sleep_in_on_weekend中编写了要求#1。为实现这一目标,我们可以编写的代码量最少?也许:

    package com.codingbat.practice;
    
    public class SleepIn {
    
        public SleepIn() {
        }
    
        public boolean sleepIn(boolean weekday, boolean vacation) {
            return !weekday;
        }
    }
    

    继续讨论下一个要求,比如我们在单元测试can_sleep_in_during_vacation中将其编成法典。通过此测试的代码量最少?

    package com.codingbat.practice;
    
    public class SleepIn {
    
        public SleepIn() {
        }
    
        public boolean sleepIn(boolean weekday, boolean vacation) {
            return !weekday || vacation;
        }
    }
    

    由于此后没有更多要求,我们已经完成了编写代码。