本讲我们将阐述编程中的一个重要环节——测试代码。【btw,在LLM时代笔者认为这比debug更加重要】
注:下述代码均在IntelliJ IDEA中编写,不保证其他IDE中有类似结果。
测试(Testing)
- 如何判断我们写的代码是否准确?在学习初期,我们往往会通过手动输入交互或将代码提交到OJ网站进行测试。
- 然而,现实中我们常常需要自己编写测试样例,甚至编写整个测试代码。
随机测试(Ad Hoc Testing)
- 我们以字符串排序为例(假设已经编写了排序代码类方法
Sort.sort(xxx)),对此可以编写如下的测试类: 编写完成后,我们可以创建一个空方法对其功能进行测试:public class TestSort { /** 测试 Sort 类的 sort 方法 */ public static void testSort() { String[] input = {"must", "be", "the", "water"}; String[] expected = {"be", "must", "the", "water"}; 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(); } } 运行public class Sort { /** 创建一个空方法 */ public static void sort(String[] x) { return; } }TestSort后会得到如下结果: 这就说明我们的测试有效。注意到上面我们使用了循环进行测试,虽然不算太复杂,但之后对大型程序再使用这种随机测试就比较繁琐。Mismatch in position 0, expected: be, but got: must. - 对此,我们考虑使用Google的
Truth库替代(可参见官网):
Truth测试
- Truth库的测试属于Java单元测试的一种(除此之外还有JUnit,AssertJ等),其原理与python的
assert语句类似,通过断言系统检测代码输出与期望输出之间的差异,在适当的时机抛出异常。 - 下面我们将上面的
TestSort类用Truth语句改写: 运行后报出的异常形式:import static com.google.common.truth.Truth.assertThat; public class testSort { public static void testSort() { String[] input = {"must", "be", "the", "water"}; String[] expected = {"be", "must", "the", "water"}; Sort.sort(input); assertThat(input).isEqualTo(expected); } public static void main(String[] args) { testSort(); } } 当然,为了更接近真实的单元测试,我们还需要对上述代码进行微调(去掉主函数,增加Exception in thread "main" expected : [be, must, the, water] but was : [must, be, the, water] differs at index: [0] at testSort.testSort(testSort.java:8) at testSort.main(testSort.java:12)@Test注解,将测试类改为非静态): 这样我们就完成了单元测试代码的编写。【当测试通过时,我们就能收获一个大大的✅️】import org.junit.jupiter.api.Test; import static com.google.common.truth.Truth.assertThat; public class TestSort { @Test public void testSort() { String[] input = {"must", "be", "the", "water"}; String[] expected = {"be", "must", "the", "water"}; Sort.sort(input); assertThat(input).isEqualTo(expected); } }
选择排序
- 在编写完测试代码后,我们就来编写用于测试的代码,即实现一个排序算法。这里我们考虑最简单的算法——选择排序(Selection Sort)算法。
- 选择排序算法主要包含下述三个步骤:
- 找出最小的元素;
- 将最小元素移到最前面(与最前面的元素交换位置);
- 对剩余的元素再进行选择排序,以此类推。
- 下面我们分别实现这三个步骤:
- 首先是找到最小元素:
我们可以编写下述测试类验证: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); // java中不能直接使用不等号比较字符串 if (cmp < 0) { smallest = x[i]; } } return smallest; }public static void testFindSmallest() { String[] input = {"i", "have", "two", "balls"}; String expected = "balls"; String actual = Sort.findSmallest(input); assertThat(actual).isEqualTo(expected); String[] input2 = {"yuri", "is", "GOAT"}; String expected2 = "GOAT"; String actual2 = Sort.findSmallest(input2); assertThat(actual2).isEqualTo(expected2); } - 然后是交换元素,这个不难实现:
注意到这里使用了字符串数组的索引,所以需要对上面的public static void swap(String[] x, int a, int b) { String temp = x[a]; x[a] = x[b]; x[b] = temp; }findSmallest类进行修改: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; } public static void testFindSmallest() { // 对应的测试类 String[] input = {"i", "have", "two", "balls"}; int expected = 3; int actual = Sort.findSmallest(input); assertThat(actual).isEqualTo(expected); String[] input2 = {"yuri", "is", "GOAT"}; int expected2 = 2; int actual2 = Sort.findSmallest(input); assertThat(actual2).isEqualTo(expected2); } - 接着我们综合使用这两个类,实现前两个步骤:
/** Sorts strings destructively. */ public static void sort(String[] x) { int smallestIndex = findSmallest(x); swap(x, 0, smallestIndex); } - 那么第三个步骤(递归迭代)如何实现?注意到在递归时我们需要不断对字符串数组取子集,所以我们需要增加一个参数
start表示当前子字符串数组起始索引: 这样我们就完整实现了选择排序算法。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; } // 对应测试类略 private static void sort(String[] x, int start) { if (start == x.length) { return; } int smallestIndex = findSmallest(x, start); swap(x, start, smallestIndex); sort(x, start + 1); } public static void sort(String[] x) { sort(x, 0); }
- 首先是找到最小元素:
- 当然,在编写代码的过程中,我们可以不断利用已有的
testSort测试类进行检验,不断调试。总而言之,综合测试和调试器,可以很大程度上方便我们排查问题,并更高效地编写代码。 - 另外,除了单元测试,有时还需要进行集成测试验证不同模块之间的交互是否正常,这里不详细展开。
