Skip to content

Commit d80b671

Browse files
authored
Merge pull request lingcoder#278 from xiangflight/master
revision[16] 截止 Preconditions
2 parents be04fc9 + d9a1d27 commit d80b671

File tree

1 file changed

+28
-38
lines changed

1 file changed

+28
-38
lines changed

docs/book/16-Validating-Your-Code.md

Lines changed: 28 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -6,33 +6,33 @@
66

77
### 你永远不能保证你的代码是正确的,你只能证明它是错的。
88

9-
让我们先暂停学习编程语言的知识,看看一些代码基础知识。特别是能让你的代码更加健壮的。
10-
9+
让我们先暂停编程语言特性的学习,看看一些代码基础知识。特别是能让你的代码更加健壮的知识。
1110

11+
<!-- Testing -->
1212

1313
## 测试
1414

1515
### 如果没有测试过,它就是不能工作的。
1616

17-
Java是一个静态类型的语言,程序员经常对一种编程语言明显的安全性过于感到舒适,“能通过编译器,那就是没问题的”。但静态类型检查是一种非常局限性的测试。这说明编译器接受你的语法和基本类型规则,但不意味着你的代码满足程序安全的目标。随着你代码经验的丰富,你逐渐了解到你的代码从来没有满足过安全性这个目标。迈向代码的第一步就是创建代码测试,争对你的目标检查代码行为
17+
Java是一个静态类型的语言,程序员经常对一种编程语言明显的安全性感到过于舒适,“能通过编译器,那就是没问题的”。但静态类型检查是一种非常局限性的测试,只是说明编译器接受你代码中的语法和基本类型规则,并不意味着你的代码达到程序的目标。随着你代码经验的丰富,你逐渐了解到你的代码从来没有满足过这些目标。迈向代码校验的第一步就是创建测试,针对你的目标检查代码的行为
1818

1919
#### 单元测试
2020

21-
这个过程是将集成测试构建到你创建的所有代码中,并在当你每次构建你的系统时运行这些测试。这样,构建过程检查不仅仅是检查语法的错误,同时你也教它检查语义错误
21+
这个过程是将集成测试构建到你创建的所有代码中,并在每次构建系统时运行这些测试。这样,构建过程不仅能检查语法的错误,同时也能检查语义的错误
2222

23-
“单元”指的是测试代码中的一小部分的想法。通常,每个类都有测试检查它所有方法的行为。“系统”测试则是不同的,它检查程序是否满足要求
23+
“单元”是指测试一小部分代码 。通常,每个类都有测试来检查它所有方法的行为。“系统”测试则是不同的,它检查的是整个程序是否满足要求
2424

25-
C风格的语言,特别是C++,通常会认为性能比安全更重要。用Java编程比C++(一般认为大概快两倍)快的原因是Java的安全性网络:这种特征类似于垃圾回收以及键入检查。通过将单元测试集成到构建过程中,你扩大了这个安全网,有了更快的开发效率。当发现设计或实现的缺陷时,可以更容易、更大胆重构你的代码
25+
C 风格的语言,尤其是 C++,通常会认为性能比安全更重要。用 Java 编程比 C++(一般认为大概快两倍)快的原因是 Java 的安全性保障:比如垃圾回收以及改良的类型检测等特性。通过将单元测试集成到构建过程中,你扩大了这个安全保障,因而有了更快的开发效率。当发现设计或实现的缺陷时,可以更容易、更大胆地重构你的代码
2626

27-
当我意识到,要确保书中代码的正确性时,我自己的测试经历就开始了。这本书通过Gradle构建系统, 你需要安装JDK,你可以通过输入gradlew compileJava编译本书的所有代码。自动提取和自动编译的效果对本书代码的质量是如此的直接和引人注目(在我看来)任何编程书籍的必备条件——你怎么能相信你没有编译的代码呢? 我发现可以利用搜索和替换在整本书大范围的修改。如果引入了一个错误,代码提取器和构建系统就会清除它。随着程序越来越复杂,我在系统发现了一个严重的漏洞。 编译程序是毫无疑问的第一步, 对于一本要出版的书而言,这看来是相当具有革命意义的发现(由于出版压力, 你经常打开一本程序设计的书并且发现了上面代码的缺陷)。尽管,我收到了来自读者反馈的语法问题。我在实现一个自动化执行测试系统的时候,使用了在早期能看到效果的步骤,但这是迫于出版压力,与此同时我明白我的程序绝对有问题,这些都会变成bug让我自食其果。我也经常收到读者的抱怨说我没有显示足够的代码输出。我需要验证程序的输出,并且在书中显示验证的输出。我以前的意见是读者应该一边看书一边运行代码,许多读者就是这么做的并且从中受益。然而,这种态度背后的原因是,我无法保证书中的输出是正确的。从经验来看,我知道随着时间的推移,会发生一些事情,使得输出不再正确(或者,一开始就没有把它弄对)。为了解决这个问题,我利用Python创建了一个工具(你将在下载的示例中找到此工具)。本书中的大多数程序都产生控制台输出,该工具将该输出与源代码清单末尾的注释中显示的预期输出进行比较,所以读者可以看到预期的输出,并且知道这个输出已经被构建程序验证的
27+
我自己的测试经历开始于我意识到要确保书中代码的正确性,书中的所有程序必须能够通过合适的构建系统自动提取、编译。这本书所使用的构建系统是 Gradle。 你只要在安装 JDK 后输入 **gradlew compileJava**,就能编译本书的所有代码。自动提取和自动编译的效果对本书代码的质量是如此的直接和引人注目(在我看来)这会很快成为任何编程书籍的必备条件——你怎么能相信没有编译的代码呢? 我还发现我可以使用搜索和替换在整本书进行大范围的修改,如果引入了一个错误,代码提取器和构建系统就会清除它。随着程序越来越复杂,我在系统中发现了一个严重的漏洞。编译程序毫无疑问是重要的第一步, 对于一本要出版的书而言,这看来是相当具有革命意义的发现(由于出版压力, 你经常打开一本程序设计的书会发现书中代码的错误)。但是,我收到了来自读者反馈代码中存在语义问题。当然,这些问题可以通过运行代码发现。我在早期实现一个自动化执行测试系统时尝试了一些不太有效的方式,但迫于出版压力,我明白我的程序绝对有问题,并会以 bug 报告的方式让我自食恶果。我也经常收到读者的抱怨说,我没有显示足够的代码输出。我需要验证程序的输出,并且在书中显示验证的输出。我以前的意见是读者应该一边看书一边运行代码,许多读者就是这么做的并且从中受益。然而,这种态度背后的原因是,我无法保证书中的输出是正确的。从经验来看,我知道随着时间的推移,会发生一些事情,使得输出不再正确(或者一开始就不正确)。为了解决这个问题,我利用 Python 创建了一个工具(你将在下载的示例中找到此工具)。本书中的大多数程序都产生控制台输出,该工具将该输出与源代码清单末尾的注释中显示的预期输出进行比较,所以读者可以看到预期的输出,并且知道这个输出已经被构建程序验证过
2828

2929
#### JUnit
3030

31-
最初的Junit发布于2000年,大概是基于Java 1.0,因此不能使用Java的反射工具。因此,用旧的JUnit编写单元测试是一项相当繁忙和冗长的工作。我发现这个设计令人不爽,并编写了自己的单元测试框架作为注解一章的示例。这个框架走向了另一个极端,“尝试最简单可行的方法”(极限编程中的一个关键短语)。从那之后,Junit通过反射和注解得到了极大的改进,这大大简化了编写单元测试代码的过程。在Java8中,他们甚至增加了对lambdas表达式的支持。本书使用当时最新的Junit5版本
31+
最初的 JUnit 发布于 2000 年,大概是基于 Java 1.0,因此不能使用 Java 的反射工具。因此,用旧的 JUnit 编写单元测试是一项相当繁忙和冗长的工作。我发现这个设计令人不爽,并编写了自己的单元测试框架作为[注解](./Annotations.md)一章的示例。这个框架走向了另一个极端,“尝试最简单可行的方法”(极限编程中的一个关键短语)。从那之后,JUnit 通过反射和注解得到了极大的改进,大大简化了编写单元测试代码的过程。在 Java8 中,他们甚至增加了对 lambdas 表达式的支持。本书使用当时最新的 Junit5 版本
3232

33-
在JUnit最简单的使用中,使用 **@Test** 注解标记表示测试的每个方法。JUnit将这些方法标识为单独的测试,并一次设置和运行一个测试,采取措施避免测试之间的副作用。
33+
在 JUnit 最简单的使用中,使用 **@Test** 注解标记表示测试的每个方法。JUnit 将这些方法标识为单独的测试,并一次设置和运行一个测试,采取措施避免测试之间的副作用。
3434

35-
让我们尝试一个简单的例子。**CountedList** 继承 **ArrayList** ,添加信息来追踪有多少个**CountedLists**被创建:
35+
让我们尝试一个简单的例子。**CountedList** 继承 **ArrayList** ,添加信息来追踪有多少个 **CountedLists** 被创建:
3636

3737
```java
3838
// validating/CountedList.java
@@ -43,13 +43,13 @@ public class CountedList extends ArrayList<String> {
4343
private static int counter = 0;
4444
private int id = counter++;
4545
public CountedList() {
46-
System.out.println("CountedList #" + id);
46+
System.out.println("CountedList #" + id);
4747
}
4848
public int getId() { return id; }
4949
}
5050
```
5151

52-
标准实例是将测试放在它们自己的子目录中。测试还必须放在包中,以便JUnit能够发现它们:
52+
标准做法是将测试放在它们自己的子目录中。测试还必须放在包中,以便 JUnit 能发现它们:
5353

5454
```java
5555
// validating/tests/CountedListTest.java
@@ -62,20 +62,20 @@ public class CountedListTest {
6262
private CountedList list;
6363
@BeforeAll
6464
static void beforeAllMsg() {
65-
System.out.println(">>> Starting CountedListTest");
65+
System.out.println(">>> Starting CountedListTest");
6666
}
6767

6868
@AfterAll
6969
static void afterAllMsg() {
70-
System.out.println(">>> Finished CountedListTest");
70+
System.out.println(">>> Finished CountedListTest");
7171
}
7272

7373
@BeforeEach
7474
public void initialize() {
7575
list = new CountedList();
7676
System.out.println("Set up for " + list.getId());
7777
for(int i = 0; i < 3; i++)
78-
list.add(Integer.toString(i));
78+
list.add(Integer.toString(i));
7979
}
8080

8181
@AfterEach
@@ -110,8 +110,8 @@ private CountedList list;
110110

111111
@Test
112112
public void order() {
113-
System.out.println("Running testOrder()");
114-
compare(list, new String[] { "0", "1", "2" });
113+
System.out.println("Running testOrder()");
114+
compare(list, new String[] { "0", "1", "2" });
115115
}
116116

117117
@Test
@@ -162,45 +162,35 @@ Cleaning up 4
162162

163163
**@BeforeAll** 注解是在任何其他测试操作之前运行一次的方法。 **@AfterAll** 是所有其他测试操作之后只运行一次的方法。两个方法都必须是静态的。
164164

165-
**@BeforeEach**注解是通常用于创建和初始化公共对象的方法,并在每次测试前运行。可以将所有这样的初始化放在test类的构造函数中,尽管我认为 **@BeforeEach** 更加清晰。JUnit为每个测试创建一个对象,确保测试运行之间没有副作用。然而,所有测试的所有对象都是同时创建的(而不是在测试之前创建对象),所以使用 **@BeforeEach** 和构造函数之间的唯一区别是 **@BeforeEach** 在测试前直接调用。在大多数情况下,这不是问题,如果你愿意,可以使用构造函数方法。
165+
**@BeforeEach**注解是通常用于创建和初始化公共对象的方法,并在每次测试前运行。可以将所有这样的初始化放在测试类的构造函数中,尽管我认为 **@BeforeEach** 更加清晰。JUnit为每个测试创建一个对象,确保测试运行之间没有副作用。然而,所有测试的所有对象都是同时创建的(而不是在测试之前创建对象),所以使用 **@BeforeEach** 和构造函数之间的唯一区别是 **@BeforeEach** 在测试前直接调用。在大多数情况下,这不是问题,如果你愿意,可以使用构造函数方法。
166166

167-
如果你必须在每次测试后执行清理(如果修改了需要恢复的静态文件,打开文件需要关闭,打开数据库或者网络连接,etc),那就用注解 **@AfterEach**.
167+
如果你必须在每次测试后执行清理(如果修改了需要恢复的静态文件,打开文件需要关闭,打开数据库或者网络连接,etc),那就用注解 **@AfterEach**
168168

169-
每个测试创建一个新的 **CountedListTest** 对象,任何非静态成员变量也会在同一时间创建。然后为每个测试调用 **initialize()**于是list被分配了一个新的 **CountedList** 对象,然后用 **String“0”、“1”****“2”** 初始化。观察 **@BeforeEach****@AfterEach** 的行为,这些方法在初始化和清理测试时显示有关测试的信息。
169+
每个测试创建一个新的 **CountedListTest** 对象,任何非静态成员变量也会在同一时间创建。然后为每个测试调用 **initialize()**于是 list 被赋值为一个新的用字符串“0”、“1” 和 “2” 初始化的 **CountedList** 对象。观察 **@BeforeEach****@AfterEach** 的行为,这些方法在初始化和清理测试时显示有关测试的信息。
170170

171-
**insert()****replace()** 演示了典型的测试方法。JUnit使用 **@Test** 注解发现这些方法,并将每个方法作为测试运行。在方法内部,你可以执行任何所需的操作并使用 JUnit 断言方法("assert"开头)验证测试的正确性(更全面的"assert"说明可以在Junit文档里找到)。如果断言失败,将显示导致失败的表达式和值。这通常就足够了,但是你也可以使用每个JUnit断言语句的重载版本,它包含一个字符串,以便在断言失败时显示。
171+
**insert()****replace()** 演示了典型的测试方法。JUnit 使用 **@Test** 注解发现这些方法,并将每个方法作为测试运行。在方法内部,你可以执行任何所需的操作并使用 JUnit 断言方法("assert"开头)验证测试的正确性(更全面的"assert"说明可以在 Junit 文档里找到)。如果断言失败,将显示导致失败的表达式和值。这通常就足够了,但是你也可以使用每个 JUnit 断言语句的重载版本,它包含一个字符串,以便在断言失败时显示。
172172

173173
断言语句不是必须的;你可以在没有断言的情况下运行测试,如果没有异常,则认为测试是成功的。
174174

175-
**compare()** 是“helper方法”的一个例子,它不是由JUnit执行的,而是被类中的其他测试使用。只要没有**@Test** 注解,Junit就不会运行它,也不需要特定的签名。在这, **compare()****private** ,表示在测试类使用,但他同样可以是 **public** 。其余的测试方法通过将其重构为 **compare()** 方法来消除重复的代码。
176-
177-
本书使用**build.gradle**控制测试,运行本章节的测试,命令:
178-
179-
**gradlew validating:test**
175+
**compare()** 是“helper方法”的一个例子,它不是由 JUnit 执行的,而是被类中的其他测试使用。只要没有 **@Test** 注解,JUnit 就不会运行它,也不需要特定的签名。在这里,**compare()** 是私有方法 ,表示仅在测试类中使用,但他同样可以是 **public** 。其余的测试方法通过将其重构为 **compare()** 方法来消除重复的代码。
180176

181-
Gradle不会运行已经运行过的测试,所以如果你没有得到测试结果,先运行:
177+
本书使用 **build.gradle** 控制测试,运行本章节的测试,使用命令:`gradlew validating:test`,Gradle 不会运行已经运行过的测试,所以如果你没有得到测试结果,得先运行:`gradlew validating:clean`
182178

183-
**gradlew validating:clean**
184-
185-
可以用这个命令运行本书的所有测试:
179+
可以用下面这个命令运行本书的所有测试:
186180

187181
**gradlew test**
188182

189-
尽管可以用最简单的方法,如CountedListTest.java所示
190-
191-
JUnit包含许多额外的测试业务,你可以在其上了解这些结构
192-
193-
[junit.org]: junit.org.
183+
尽管可以用最简单的方法,如 **CountedListTest.java** 所示没那样,JUnit 还包括大量的测试结构,你可以到[官网](junit.org)上学习它们。
194184

195-
Junit是Java最流行的单元测试框架,但也有其它可以替代的。你可以通过互联网发现更适合的那一个。
185+
JUnit 是 Java 最流行的单元测试框架,但也有其它可以替代的。你可以通过互联网发现更适合的那一个。
196186

197187
#### 测试覆盖率的幻觉
198188

199189
测试覆盖率,同样也称为代码覆盖率,度量代码的测试百分比。百分比越高,测试的覆盖率越大。这里有很多[方法](https://en.wikipedia.org/wiki/Code_coverage)
200190

201-
计算覆盖率,还有有帮助的文章[Java代码覆盖工具](https://en.wikipedia.org/wiki/Java_Code_Coverage_Tools)
191+
计算覆盖率,还有有帮助的文章[Java代码覆盖工具](https://en.wikipedia.org/wiki/Java_Code_Coverage_Tools)
202192

203-
对于没有知识但处于控制地位的人来说,很容易在没有任何了解的情况下也有概念认为100%的测试覆盖是唯一可接受的值。这有一个问题,因为100%并不意味着是对测试有效性的最佳测量。你可以测试所有需要它的东西,但是只需要65%的覆盖率。如果需要100%的覆盖,你将浪费大量时间来生成剩余的代码,并且在向项目添加代码时浪费的时间更多。
193+
对于没有知识但处于控制地位的人来说,很容易在没有任何了解的情况下也有概念认为 100% 的测试覆盖是唯一可接受的值。这有一个问题,因为 100% 并不意味着是对测试有效性的良好测量。你可以测试所有需要它的东西,但是只需要 65% 的覆盖率。如果需要 100% 的覆盖,你将浪费大量时间来生成剩余的代码,并且在向项目添加代码时浪费的时间更多。
204194

205195
当分析一个未知的代码库时,测试覆盖率作为一个粗略的度量是有用的。如果覆盖率工具报告的值特别低(比如,少于百分之40),则说明覆盖不够充分。然而,一个非常高的值也同样值得怀疑,这表明对编程领域了解不足的人迫使团队做出了武断的决定。覆盖工具的最佳用途是发现代码库中未测试的部分。但是,不要依赖覆盖率来得到测试质量的任何信息。
206196

0 commit comments

Comments
 (0)