调试程序
可以说是最能体现程序员水平的能力之一了,接下来我们将讨论如何写test来保证你的程序的正确性,与此同时我们将讨论一种比较算法——选择排序
。
1. Ad Hoc Testing
在写sort类之前,我们先来设想并实现TestSort类。
public class TestSort {
/** Tests the sort method of the Sort class. */
public static void testSort() {
String[] input = {"i", "have", "an", "egg"};
String[] expected = {"an", "egg", "have", "i"};
Sort.sort(input);
for (int i = 0; i < input.length; i += 1) {
if (!input[i].equals(expected[i])) {
System.out.println("Mismatch in position " + i + ", expected: " + expected + ", but got: " + input[i] + ".");
break;
}
}
}
public static void main(String[] args) {
testSort();
}
}
然后,我们写一个空的sort方法,先不去实现它。
public class Sort {
/** Sorts strings destructively. */
public static void sort(String[] x) {
}
}
于是,当我们运行testsort()
方法并调用Sort.sort()
方法时,控制台将会显示如下结果:
Mismatch in position 0, expected: an, but got: i.
我们的到了一个报错信息,这证明我们写的测试类是正确的。事实上,这特别想课堂上的一些自动打分系统(autograder),你已经可以更准确客观地判断你的代码的正确性了。
不过,写上面这样的测试类有些麻烦,你必须写很多的循环打印各种各样的信息,甚至测试类比你真正要写的类还要长,所以,我们将使用org.junit
库来简化我们的工作。
2. JUnit Testing
在写测试类时,org.junit
库提供给我们许多有帮助的方法和使用的功能,比如,我们可以这样简化ad hoc
test:
public static void testSort() {
String[] input = {"i", "have", "an", "egg"};
String[] expected = {"an", "egg", "have", "i"};
Sort.sort(input);
org.junit.Assert.assertArrayEquals(expected, input);
}
This code is much simpler,我们再运行一下test类,将得到如下结果:
Exception in thread "main" arrays first differed at element [0]; expected:<[an]> but was:<[i]>
at org.junit.internal.ComparisonCriteria.arrayEquals(ComparisonCriteria.java:55)
at org.junit.Assert.internalArrayEquals(Assert.java:532)
...
我们得到了同样的结果,只不过这样的输出格式有些丑,我们一会会使它变得简洁一些。
3. Selection Sort
现在,我们要实现一下Sort.sort
方法,选择排序包含以下三步:
- 找到中最小的值
- 把它移动到最前面
- 对剩下的n-1个元素进行上两步操作
public class Sort {
/** Sorts strings destructively. */
public static void sort(String[] x) {
// find the smallest item
// move it to the front
// selection sort the rest (using recursion?)
}
}
可能你会觉得选择排序非常简单,不过我们还是要一步一步实现它,并在实现过程中故意犯一些错误,因为这节课的重点是讨论如何调试代码
,而不是写出更好的选择排序。
3.1 findSmallest
我们先来实现findSmallest
方法:
/** Returns the smallest string in x. */
public static String findSmallest(String[] x) {
return x[3];
}
显然,这是个错误答案,不过我们是想看看测试类会提示我们什么信息:
public class TestSort {
...
public static void testFindSmallest() {
String[] input = {"i", "have", "an", "egg"};
String expected = "an";
String actual = Sort.findSmallest(input);
org.junit.Assert.assertEquals(expected, actual);
}
public static void main(String[] args) {
testFindSmallest(); // note: we changed this from testSort!
}
}
Exception in thread "main" java.lang.AssertionError: expected:<[an]> but was:<[null]>
at org.junit.Assert.failNotEquals(Assert.juava:834)
at TestSort.testFindSmallest(TestSort.java:9)
at TestSort.main(TestSort.java:24)
接下来我们实现findSmallest
方法:
/** Returns the smallest string in x.
* @source Got help with string compares from https://goo.gl/a7yBU5. */
public static String findSmallest(String[] x) {
String smallest = x[0];
for (int i = 0; i < x.length; i += 1) {
int cmp = x[i].compareTo(smallest);
if (cmp < 0) {
smallest = x[i];
}
}
return smallest;
}
3.2 Swap
swap
方法很简单,我们可以快速实现它:
public static void swap(String[] x, int a, int b) {
String temp = x[a];
x[a] = x[b];
x[b] = temp;
}
但我们还是实验一下,如果我们像下面这样写,运行测试类,会发生什么结果:
public static void swap(String[] x, int a, int b) {
x[a] = x[b];
x[b] = x[a];
}
public class TestSort {
...
/** Test the Sort.swap method. */
public static void testSwap() {
String[] input = {"i", "have", "an", "egg"};
int a = 0;
int b = 2;
String[] expected = {"an", "have", "i", "egg"};
Sort.swap(input, a, b);
org.junit.Assert.assertArrayEquals(expected, input);
}
public static void main(String[] args) {
testSwap();
}
}
Exception in thread "main" arrays first differed in element [2]; expected:<[i]> but was:<[an]>
at TestSort.testSwap(TestSort.java:36)
3.3 Revising findSmallest
&mesp;完成上面两个方法后,我们的sort
方法变成了下面这样:
/** Sorts strings destructively. */
public static void sort(String[] x) {
// find the smallest item
String smallest = findSmallest(x);
// move it to the front
swap(x, 0, smallest);
// selection sort the rest (using recursion?)
}
显然,为了使用swap
方法,我们的findSmallest
方法要返回最小值的下标来作为参数传递给swap
方法,所以我们进行一些改进:
public static int findSmallest(String[] x) {
int smallestIndex = 0;
for (int i = 0; i < x.length; i += 1) {
int cmp = x[i].compareTo(x[smallestIndex]);
if (cmp < 0) {
smallestIndex = i;
}
}
return smallestIndex;
}
之后,最好更改测试类,来测试我们改动的正确性。
3.4 Recursive Helper Methods
我们将使用递归的方法完成最后一步,因此需要设计一个递归的辅助方法,我们选择增加一个重载的sort
方法:
/** Sorts strings destructively starting from item start. */
private static void sort(String[] x, int start) {
int smallestIndex = findSmallest(x);
swap(x, start, smallestIndex);
sort(x, start + 1);
}
可是运行测试类,我们会遇到这样一个问题:
Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: 4
at Sort.swap(Sort.java:16)
使用编译器的debug可以发现,我们没有考虑递归结束的临界条件,于是完善这个方法:
/** Sorts strings destructively starting from item start. */
private static void sort(String[] x, int start) {
if (start == x.length) {
return;
}
int smallestIndex = findSmallest(x);
swap(x, start, smallestIndex);
sort(x, start + 1);
}
运行测试类,我们有会得到一个报错:
Exception in thread "main" arrays first differed at element [0];
expected<[an]> bit was:<[have]>
再进行断点调试,发现findSmallest
方法除了问题,每次调用都是在整个数组范围进行比较,所以,我们增加一个参数指定比较范围的开始位置:
public static int findSmallest(String[] x, int start) {
int smallestIndex = start;
for (int i = start; i < x.length; i += 1) {
int cmp = x[i].compareTo(x[smallestIndex]);
if (cmp < 0) {
smallestIndex = i;
}
}
return smallestIndex;
}
每改变一个方法都要用为这个方法设计的测试方法测试,先用自定义的testFindSmallest
方法测试,通过后再用testsort
方法测试,最后应该会全部通过,到此为止,我们成功实现了选择排序方法。
4. Better JUnit
在日常调试中,我们通常会进行如下操作简化测试类的编写:
- 把上述所有静态方法变成非静态
- 在测试类中导包:
import org.junit.Test
import static org.junit.Assert.* - 在每个测试方法前面加标签
@Test
,并删除原本用于测试的main方法 - 将冗长的调用语句
org.junit.Assert.assertEquals(expected, actual);
改为简单的assertEquals(expected, actual);
5. Testing Philosophy
一般程序员测试程序的构思分为三种。
一种是Autograder Driven Development
(ADD),这样的思路一般是先写出tons of codes,然后测试后发现有问题,再一步一步的找出来,很可能错误百出,丧失了debug的信心。
一种是Test-Driven Development
(TDD),这种是把程序分为一个个小的单元,每完成一个单元就用测试类测试写的是否正确,不过这种方式会浪费时间,拖慢工作节奏。
还有一种是Integration Test
,也就是集成测试,与单元测试类似的是,这种方法也是看代码是否按预期工作,不过集成测试范围广泛,一般会涉及到外部组件,且设计难度较高,一般由专门的测试人员完成。