什么是单元测试

这是一个初级读本。它主要是因为文档被迫有一个例子,即使它是作为存根文章。如果你已经了解了单元测试的基础知识,请随时跳到提及特定框架的备注。

单元测试确保给定模块按预期运行。在大规模应用中,确保在真空中适当执行模块是确保应用程序保真度的不可或缺的部分。

考虑以下(普通)伪示例:

public class Example {
  public static void main (String args[]) {
    new Example();
  }

  // Application-level test.
  public Example() {
    Consumer c = new Consumer();
    System.out.println("VALUE = " + c.getVal());
  }

  // Your Module.
  class Consumer {
    private Capitalizer c;
  
    public Consumer() {
      c = new Capitalizer();
    }

    public String getVal() {
      return c.getVal();
    }
  }

  // Another team's module.
  class Capitalizer {
    private DataReader dr;
  
    public Capitalizer() {
      dr = new DataReader();
    }

    public String getVal() {
      return dr.readVal().toUpperCase();
    }
  }

  // Another team's module.
  class DataReader {
    public String readVal() {
      // Refers to a file somewhere in your application deployment, or
      // perhaps retrieved over a deployment-specific network.
      File f; 
      String s = "data";
      // ... Read data from f into s ...
      return s;
    }
  }
}

所以这个例子是微不足道的; DataReader 从文件中获取数据,将其传递给 CapitalizerCapitalizer 将所有字符转换为大写,然后传递给 Consumer。但是 DataReader 与我们的应用程序环境密切相关,因此我们推迟测试此链,直到我们准备部署测试版本。

现在,假设,在发布中的某个地方,由于未知的原因,Capitalizer 中的 getVal() 方法从返回 toUpperCase() 字符串变为 toLowerCase() 字符串:

  // Another team's module.
  class Capitalizer {
    ...

    public String getVal() {
      return dr.readVal().toLowerCase();
    }
  }

显然,这打破了预期的行为。但是,由于执行 DataReader 涉及的过程繁重,我们在下一次测试部署之前不会注意到这一点。因此,几天/几周/几个月,我们的系统中会出现这个错误,然后产品经理会看到这一点,并立即转向你,即与 Consumer 相关的团队负责人。 “为什么会这样?你们有什么改变?” 显然,你是无能为力的。你不知道发生了什么。你没有改变任何应该触及的代码; 它为什么突然坏了?

最终,在团队和协作之间的讨论之后,问题被追踪,并且问题得到解决。但是,这引出了一个问题; 怎么可以防止这种情况?

有两件明显的事情:

测试需要自动化

我们对手动测试的依赖使得这个 bug 在很长时间内都没有引起注意。我们需要一种方法来自动化立即引入错误的过程。从现在开始不到 5 周。从现在开始不是 5 天。距离现在不到 5 分钟。马上。

你必须要明白,在这个例子中,我已经表达了一个引入和忽视的非常微不足道的错误。在工业应用中,随着数十个模块不断更新,这些模块可以遍布各处。你用一个模块来修复某些东西,只是意识到你在其他地方(内部或外部)以某种方式依赖你修复的行为。

如果没有严格的验证,事情就会蔓延到系统中。有可能的是,如果忽略得足够远,这将导致如此多的额外工作试图修复变更(然后修复这些修复等),产品实际上会增加剩余的工作量。你不想处于这种情况。

测试需要细化

我们上面的例子中提到的第二个问题是跟踪 bug 所花费的时间。当测试人员发现它时,产品经理给你发了戳,你调查并发现 Capitalizer 正在返回看似不好的数据,你用你的发现,他们调查等等来 ping 了 Capitalizer 团队等。

我在上面提到的关于这个微不足道的例子的数量和难度的观点与此相同。显然,任何理解精通 Java 的人都可以快速找到引入的问题。但追踪和沟通问题通常要困难得多。也许 Capitalizer 团队为你提供了没有来源的 JAR。也许他们位于世界的另一边,沟通时间非常有限(也许是每天发送一次的电子邮件)。它可能导致需要数周或更长时间才能跟踪的错误(同样,对于给定的版本,可能会有几个错误)。

为了减轻这种影响,我们希望在尽可能精细的水平上进行严格的测试 (你还需要进行粗粒度测试以确保模块正常交互,但这不是我们的焦点)。我们希望严格指定所有外向功能(至少)如何运行,以及对该功能的测试。

进入单元测试

想象一下,如果我们进行了测试,特别是确保 CapitalizergetVal() 方法返回给定输入字符串的大写字符串。此外,假设测试在我们提交任何代码之前运行。引入系统的错误(即 toUpperCase()toLowerCase() 取代)不会引起任何问题,因为错误永远不会引入系统。我们会在测试中发现它,开发人员(希望)会意识到他们的错误,并且会就如何引入他们的预期效果达成替代解决方案。

这里有一些关于如何实现这些测试的遗漏,但这些遗漏在特定于框架的文档中(在备注中链接)。我们希望,这作为一个例子**,为什么**单元测试是非常重要的。