mirror of
https://github.com/LingCoder/OnJava8.git
synced 2026-08-24 06:53:27 +08:00
Update 16-Validating-Your-Code.md
This commit is contained in:
@@ -30,9 +30,9 @@ C风格的语言,特别是C++,通常会认为性能比安全更重要。用J
|
||||
|
||||
最初的Junit发布于2000年,大概是基于Java 1.0,因此不能使用Java的反射工具。因此,用旧的JUnit编写单元测试是一项相当繁忙和冗长的工作。我发现这个设计令人不爽,并编写了自己的单元测试框架作为注解一章的示例。这个框架走向了另一个极端,“尝试最简单可行的方法”(极限编程中的一个关键短语)。从那之后,Junit通过反射和注解得到了极大的改进,这大大简化了编写单元测试代码的过程。在Java8中,他们甚至增加了对lambdas表达式的支持。本书使用当时最新的Junit5版本
|
||||
|
||||
在JUnit最简单的使用中,使用**@Test**注解标记表示测试的每个方法。JUnit将这些方法标识为单独的测试,并一次设置和运行一个测试,采取措施避免测试之间的副作用。
|
||||
在JUnit最简单的使用中,使用 **@Test** 注解标记表示测试的每个方法。JUnit将这些方法标识为单独的测试,并一次设置和运行一个测试,采取措施避免测试之间的副作用。
|
||||
|
||||
让我们尝试一个简单的例子。**CountedLis**t继承**ArrayList**,添加信息来追踪有多少个**CountedLists**被创建:
|
||||
让我们尝试一个简单的例子。**CountedList** 继承 **ArrayList** ,添加信息来追踪有多少个**CountedLists**被创建:
|
||||
|
||||
```java
|
||||
// validating/CountedList.java
|
||||
@@ -160,19 +160,19 @@ Cleaning up 4
|
||||
*/
|
||||
```
|
||||
|
||||
**@BeforeAll**注解是在任何其他测试操作之前运行一次的方法。**@AfterAll**是所有其他测试操作之后只运行一次的方法。两个方法都必须是静态的。
|
||||
**@BeforeAll** 注解是在任何其他测试操作之前运行一次的方法。 **@AfterAll** 是所有其他测试操作之后只运行一次的方法。两个方法都必须是静态的。
|
||||
|
||||
**@BeforeEach**注解是通常用于创建和初始化公共对象的方法,并在每次测试前运行。或者,您可以将所有这样的初始化放在test类的构造函数中,尽管我认为**@BeforeEach**更加清晰。JUnit为每个测试创建一个对象,以确保测试运行之间没有副作用。然而,所有测试的所有对象都是同时创建的(而不是在测试之前创建对象),所以使用**@BeforeEach**和构造函数之间的唯一区别是**@BeforeEach**在测试前直接调用。在大多数情况下,这不是问题,如果您愿意,可以使用构造函数方法。
|
||||
**@BeforeEach**注解是通常用于创建和初始化公共对象的方法,并在每次测试前运行。或者,您可以将所有这样的初始化放在test类的构造函数中,尽管我认为 **@BeforeEach** 更加清晰。JUnit为每个测试创建一个对象,以确保测试运行之间没有副作用。然而,所有测试的所有对象都是同时创建的(而不是在测试之前创建对象),所以使用 **@BeforeEach** 和构造函数之间的唯一区别是 **@BeforeEach** 在测试前直接调用。在大多数情况下,这不是问题,如果您愿意,可以使用构造函数方法。
|
||||
|
||||
如果您必须在每次测试后执行清理(如果修改了需要恢复的静态文件,打开文件需要关闭,打开数据库或者网络连接,etc),那就用注解**@AfterEach**.
|
||||
如果您必须在每次测试后执行清理(如果修改了需要恢复的静态文件,打开文件需要关闭,打开数据库或者网络连接,etc),那就用注解 **@AfterEach**.
|
||||
|
||||
每个测试创建一个新的**CountedListTest**对象,任何非静态成员变量也会在同一时间创建。然后为每个测试调用**initialize()**,于是list被分配了一个新的**CountedList**对象,然后用**String“0”、“1”**和**“2”**初始化。观察**@BeforeEach**和**@AfterEach**的行为,这些方法在初始化和清理测试时显示有关测试的信息。
|
||||
每个测试创建一个新的 **CountedListTest** 对象,任何非静态成员变量也会在同一时间创建。然后为每个测试调用 **initialize()** ,于是list被分配了一个新的 **CountedList** 对象,然后用 **String“0”、“1”** 和 **“2”** 初始化。观察 **@BeforeEach** 和 **@AfterEach** 的行为,这些方法在初始化和清理测试时显示有关测试的信息。
|
||||
|
||||
**insert()**和**replace()**演示了典型的测试方法。JUnit使用**@Test**注解发现这些方法,并将每个方法作为测试运行。在方法内部,您可以执行任何所需的操作并使用 JUnit 断言方法(已"assert"开头)验证测试的正确性(更全面的"assert"说明可以在Junit文档里找到)。如果断言失败,将显示导致失败的表达式和值。这通常就足够了,但是你也可以使用每个JUnit断言语句的重载版本,它包含一个字符串,以便在断言失败时显示。
|
||||
**insert()** 和 **replace()** 演示了典型的测试方法。JUnit使用 **@Test** 注解发现这些方法,并将每个方法作为测试运行。在方法内部,您可以执行任何所需的操作并使用 JUnit 断言方法(已"assert"开头)验证测试的正确性(更全面的"assert"说明可以在Junit文档里找到)。如果断言失败,将显示导致失败的表达式和值。这通常就足够了,但是你也可以使用每个JUnit断言语句的重载版本,它包含一个字符串,以便在断言失败时显示。
|
||||
|
||||
断言语句不是必须的;你可以在没有断言的情况下运行测试,如果没有异常,则认为测试是成功的。
|
||||
|
||||
**compare()**是“helper方法”的一个例子,它不是由JUnit执行的,而是被类中的其他测试使用。只要没有**@Test**注解,Junit就不会运行它,也不需要特定的签名。在这,**compare()**是**private**,表示在测试类使用,但他同样可以是**public**。其余的测试方法通过将其重构为**compare()**方法来消除重复的代码。
|
||||
**compare()** 是“helper方法”的一个例子,它不是由JUnit执行的,而是被类中的其他测试使用。只要没有**@Test** 注解,Junit就不会运行它,也不需要特定的签名。在这, **compare()** 是 **private** ,表示在测试类使用,但他同样可以是 **public** 。其余的测试方法通过将其重构为 **compare()** 方法来消除重复的代码。
|
||||
|
||||
本书使用**build.gradle**控制测试,运行本章节的测试,命令:
|
||||
|
||||
@@ -250,9 +250,9 @@ at Assert1.main(Assert1.java:9)
|
||||
*/
|
||||
```
|
||||
|
||||
如果你正常运行程序,没有任何特殊的断言标志,则不会发生任何事情。你需要在运行程序时显式启用断言。一种简单的方法是使用 **-ea** flag, 它也可以表示为: **-enableassertion**, 这将运行程序并执行任何断言语句。
|
||||
如果你正常运行程序,没有任何特殊的断言标志,则不会发生任何事情。你需要在运行程序时显式启用断言。一种简单的方法是使用 **-ea** flag, 它也可以表示为: **-enableassertion** , 这将运行程序并执行任何断言语句。
|
||||
|
||||
输出中并没有包含多少有用的信息。另一方面,如果你使用**information-expression**, 你将生成一条有用的消息作为异常堆栈跟踪的一部分。最有用的**information-expression**通常是一串针对程序员的文本:
|
||||
输出中并没有包含多少有用的信息。另一方面,如果你使用 **information-expression** , 你将生成一条有用的消息作为异常堆栈跟踪的一部分。最有用的 **information-expression** 通常是一串针对程序员的文本:
|
||||
|
||||
```java
|
||||
// validating/Assert2.java
|
||||
@@ -274,11 +274,11 @@ at Assert2.main(Assert2.java:8)
|
||||
*/
|
||||
```
|
||||
|
||||
**information-expression**可以产生任何类型的对象,因此,通常你将构造一个包含对象值的更复杂的字符串,它是否与失败的断言有关。
|
||||
**information-expression** 可以产生任何类型的对象,因此,通常你将构造一个包含对象值的更复杂的字符串,它是否与失败的断言有关。
|
||||
|
||||
还可以通过类名或包名打开或关闭断言;也就是说,您可以为整个包启用或禁用断言。实现这一点的详细信息在JDK的断言文档中。您想要打开或关闭某些断言时,此特性对于使用断言进行工具化的大型项目非常有用。然而,日志记录(*Logging*)或者调试(*Debugging*),可能是捕获这类信息的更好工具。
|
||||
|
||||
这有另一种办法控制你的断言:编程方式,通过链接到类加载器对象(**ClassLoader**)。类加载器中有几种方法允许动态启用和禁用断言,其中**setDefaultAssertionStatus ()**,它为之后加载的所有类设置断言状态。因此,你可以认为你像下面这样悄悄地开启了断言:
|
||||
这有另一种办法控制你的断言:编程方式,通过链接到类加载器对象(**ClassLoader**)。类加载器中有几种方法允许动态启用和禁用断言,其中 **setDefaultAssertionStatus ()** ,它为之后加载的所有类设置断言状态。因此,你可以认为你像下面这样悄悄地开启了断言:
|
||||
|
||||
```java
|
||||
// validating/LoaderAssertions.java
|
||||
@@ -308,7 +308,7 @@ LoaderAssertions.main(LoaderAssertions.java:9)
|
||||
*/
|
||||
```
|
||||
|
||||
这消除了在运行程序时在命令行上使用**-ea**标志的需要,使用**-ea**标志启用断言可能同样简单。当交付独立产品时,您可能必须设置一个执行脚本让用户能够启动程序,配置其他启动参数。这是有道理的,然而,决定在程序运行时启用断言可以使用下面的**static**块来实现这一点,该语句位于系统的主类中:
|
||||
这消除了在运行程序时在命令行上使用 **-ea** 标志的需要,使用 **-ea** 标志启用断言可能同样简单。当交付独立产品时,您可能必须设置一个执行脚本让用户能够启动程序,配置其他启动参数。这是有道理的,然而,决定在程序运行时启用断言可以使用下面的 **static** 块来实现这一点,该语句位于系统的主类中:
|
||||
|
||||
```java
|
||||
static {
|
||||
@@ -322,13 +322,13 @@ static {
|
||||
|
||||
|
||||
|
||||
如果启用断言,然后执行**assert**语句,**assertionsEnabled**变为**true**。断言不会失败,因为分配的返回值是赋值的值。如果不启用断言,**assert**语句不执行,**assertionsEnabled**保持false,将导致异常。
|
||||
如果启用断言,然后执行 **assert** 语句,**assertionsEnabled** 变为 **true** 。断言不会失败,因为分配的返回值是赋值的值。如果不启用断言,**assert** 语句不执行,**assertionsEnabled** 保持false,将导致异常。
|
||||
|
||||
|
||||
|
||||
#### Guava断言
|
||||
|
||||
因为启用Java本地断言很麻烦,Guava团队添加一个始终启用替换断言的**Verify**类。他们建议静态导入**Verify**方法:
|
||||
因为启用Java本地断言很麻烦,Guava团队添加一个始终启用替换断言的 **Verify** 类。他们建议静态导入 **Verify** 方法:
|
||||
|
||||
```java
|
||||
// validating/GuavaAssertions.java
|
||||
@@ -385,13 +385,13 @@ Shouldn't be null: arg s
|
||||
|
||||
|
||||
|
||||
这里有两个方法,使用变量**verify()**和**verifyNotNull()**来支持有用的错误消息。注意,**verifyNotNull()**内置的错误消息通常就足够了,而**verify()**太一般,没有有用的默认错误消息。
|
||||
这里有两个方法,使用变量 **verify()** 和 **verifyNotNull()** 来支持有用的错误消息。注意,**verifyNotNull()** 内置的错误消息通常就足够了,而 **verify()** 太一般,没有有用的默认错误消息。
|
||||
|
||||
|
||||
|
||||
#### 使用断言进行契约式设计
|
||||
|
||||
*契约式设计(DbC)*是Bertrand Meyer提出的一个概念,Eiffel语言的发明者,通过确保对象遵循某些规则来帮助创建健壮的程序。这些规则是由正在解决的问题的性质决定的,这超出了编译器可以验证的范围。虽然断言没有直接实现**DBC**(Eiffel也是如此),但是它们创建了一种非正式的DBC编程风格。DbC假定服务供应商与该服务的消费者或客户之间存在明确指定的契约。在面向对象编程中,服务通常由对象提供,对象的边界 — 供应商和消费者之间的划分 — 是对象类的接口。当客户端调用特定的公共方法时,它们希望该调用具有特定的行为:对象状态改变,以及一个可预测的返回值。
|
||||
*契约式设计(DbC)*是Bertrand Meyer提出的一个概念,Eiffel语言的发明者,通过确保对象遵循某些规则来帮助创建健壮的程序。这些规则是由正在解决的问题的性质决定的,这超出了编译器可以验证的范围。虽然断言没有直接实现 **DBC**(Eiffel也是如此),但是它们创建了一种非正式的DBC编程风格。DbC假定服务供应商与该服务的消费者或客户之间存在明确指定的契约。在面向对象编程中,服务通常由对象提供,对象的边界 — 供应商和消费者之间的划分 — 是对象类的接口。当客户端调用特定的公共方法时,它们希望该调用具有特定的行为:对象状态改变,以及一个可预测的返回值。
|
||||
|
||||
**Meyer**认为:
|
||||
|
||||
@@ -427,7 +427,7 @@ Shouldn't be null: arg s
|
||||
|
||||
此外,不变性是关于构造后对象状态的保证。
|
||||
|
||||
根据这个描述,一个有效的不变性被定义为一个方法,可能被命名为**invariant()**,它在构造之后以及每个方法的开始和结束时调用。方法可调用如下:
|
||||
根据这个描述,一个有效的不变性被定义为一个方法,可能被命名为 **invariant()** ,它在构造之后以及每个方法的开始和结束时调用。方法可调用如下:
|
||||
|
||||
assert invariant();
|
||||
|
||||
@@ -441,9 +441,9 @@ assert invariant();
|
||||
|
||||
**2**. 接下来禁用后置条件检查,当你进行合理的单元测试以验证方法是否返回了适当的值时。因为不变性检查是观察对象的状态,后置条件检查仅在方法期间验证计算结果,因此可能会被丢弃,以便进行单元测试。单元测试不会像运行时后置条件检查那样安全,但是它可能已经足够了,特别是如果你对代码有信心的话。
|
||||
|
||||
**3**. 如果你确信方法主体没有把对象改成无效状态,则可以禁用方法调用末尾的不变性检查。可以通过白盒单元测试(通过访问私有字段的单元测试来验证对象状态)来验证这一点。尽管,它可能没有调用**invariant()**那么稳妥,可以将不变性检查从运行时测试 “迁移” 到构建时测试(通过单元测试),就像使用后置条件一样。
|
||||
**3**. 如果你确信方法主体没有把对象改成无效状态,则可以禁用方法调用末尾的不变性检查。可以通过白盒单元测试(通过访问私有字段的单元测试来验证对象状态)来验证这一点。尽管,它可能没有调用 **invariant()** 那么稳妥,可以将不变性检查从运行时测试 “迁移” 到构建时测试(通过单元测试),就像使用后置条件一样。
|
||||
|
||||
**4**. 最后,万不得已,禁用前置条件检查。这是最不安全、最不明智的选择,因为尽管你知道并且可以控制你自己的代码,但是你无法控制客户端可能会传递给方法的参数。然而,**(A)**在迫切需要性能和概要分析的情况下,将前置条件检查作为瓶颈,**(B) **并且你有某种合理的保证,即客户端不会违反前置条件(如你自己编写客户端代码的情况)。禁用前置条件检查是可以接受的。
|
||||
**4**. 最后,万不得已,禁用前置条件检查。这是最不安全、最不明智的选择,因为尽管你知道并且可以控制你自己的代码,但是你无法控制客户端可能会传递给方法的参数。然而,**(A** 在迫切需要性能和概要分析的情况下,将前置条件检查作为瓶颈,**(B)** 并且你有某种合理的保证,即客户端不会违反前置条件(如你自己编写客户端代码的情况)。禁用前置条件检查是可以接受的。
|
||||
|
||||
不应该直接删除检查的代码,因为只需要禁用检查(添加注释)。这样如果发现错误,你可以轻松地恢复检查以快速发现问题。
|
||||
|
||||
|
||||
Reference in New Issue
Block a user