CS61B 课程笔记(Lecture 07)
CS61B 课程笔记(Lecture 07)
测试
3.1 一种新方式
编程中最重要的技能之一是判断代码是否正确。在这一部分,我们将探讨如何编写自己的测试,并引入一种新的思维方式。我们将先编写一个测试方法
testSort()
,然后再编写实际的排序代码。这种方法有助于确保我们的排序实现是正确的。
临时测试(Ad Hoc Testing)
下面是一个简单的测试实现示例:
1 | public class TestSort { |
运行 testSort()
方法时,如果 Sort.sort()
方法为空,将输出:
1 | Mismatch in position 0, expected: an, but got: i. |
这表明排序方法尚未实现,因此测试失败。
JUnit 测试
为了简化测试的编写,Java 提供了 org.junit
库。这个库允许我们使用断言来检查实际输出是否与预期输出相匹配。使用 JUnit
进行测试的实现如下:
1 | public static void testSort() { |
如果运行此代码时发现不匹配,会抛出如下异常:
1 | Exception in thread "main" arrays first differed at element [0]; expected:<[an]> but was:<[i]> |
这个代码片段比前面的实现更简洁,并且使用了 JUnit 提供的断言来自动检查结果。
assertArrayEquals
语法
assertArrayEquals
方法用于比较两个数组是否相等。如果两个数组的内容相同,则测试通过;如果不同,则测试失败并抛出异常。其基本语法如下:
1 org.junit.Assert.assertArrayEquals(expectedArray, actualArray);参数说明
- expectedArray:预期的数组,表示你希望排序后得到的结果。
- actualArray:实际的数组,表示经过排序方法处理后的结果。
示例代码
以下是完整的
testSort
方法示例,包含了对assertArrayEquals
的使用:
1
2
3
4
5
6
7
8
9
10
11 public static void testSort() {
// 定义输入和期望输出
String[] input = {"i", "have", "an", "egg"};
String[] expected = {"an", "egg", "have", "i"};
// 调用待测试的排序方法
Sort.sort(input);
// 使用 JUnit 的断言检查数组是否相等
org.junit.Assert.assertArrayEquals(expected, input);
}运行测试
当你运行这个测试时,如果
Sort.sort(input)
方法正确地对数组进行了排序,input
数组将与expected
数组相等,测试将通过。如果排序不正确,JUnit 将会输出错误信息,指出实际结果与预期结果不符,并显示具体的差异。错误示例
如果
Sort.sort(input)
没有正确排序,可能会看到类似以下的错误信息:
1 Exception in thread "main" arrays first differed at element [0]; expected:<[an]> but was:<[i]>这个错误信息表明在数组的第一个元素(索引为0)处,预期的值是
"an"
,但实际得到的值是"i"
。这可以帮助你快速定位问题。
选择排序(Selection Sort)
反思开发过程,拥有一组自动化测试可以帮助减轻认知负担。测试的意义在于:
- 提供信心:通过测试,我们可以对程序的基本部分充满信心,这样如果出现问题,就可以更容易找到问题所在。
- 促进重构:测试使得重构代码变得更简单。我们可以在重构代码时,依赖已有的测试来确保新代码的正确性。
更好的 JUnit
两项主要增强功能,使代码更清晰、更易于使用。
使用测试注解:
- 在每个测试方法前加上
@org.junit.Test
注解(不需要分号)。 - 将测试方法改为非静态方法。
- 从
TestSort
类中移除main
方法。这样做之后,如果在 JUnit 中使用 Run->Run 命令,所有测试会自动执行,无需手动调用。
- 在每个测试方法前加上
简化导入和调用:
在文件顶部添加导入语句:
1
2import org.junit.Test;
import static org.junit.Assert.*;使用简化的注解
@Test
替代@org.junit.Test
。这样一来,我们可以省略
org.junit.Assert.
的前缀。
测试哲学
单元测试的重要性
单元测试是软件开发中的一种关键实践,它要求开发者在编写代码之前,先清晰地定义每个代码单元(如函数或方法)的预期行为。通过这种方式,单元测试提供了一种结构化的方法来确保代码的正确性和可靠性。
主要目标
- 明确功能预期:
- 在编写测试之前,开发者需要仔细考虑方法的目的。这包括明确该方法应该完成什么任务、处理哪些输入以及预期的输出是什么。
- 这种过程促使开发者思考代码的设计,减少模糊性和不确定性。
- 定义输入和输出:
- 每个测试用例应具体描述输入数据及其类型。例如,如果一个方法接受整数列表作为输入,测试用例需要明确给定的列表内容。
- 同样,输出的预期结果也需要明确。例如,如果一个方法对列表进行排序,预期的输出应该是已排序的列表。
- 考虑边界条件:
- 边界条件是指输入数据的极限情况,例如空输入、非常大的数字、特殊字符等。
- 测试时应包括这些边界情况,以确保代码能够稳健地处理所有可能的输入。比如,如果一个函数处理数组,测试用例应包括空数组、单元素数组以及非常大的数组。
测试用例的设计
有效的测试用例设计应考虑以下要素:
- 功能性测试:确保方法按照预期工作,返回正确的结果。
- 异常情况测试:测试代码在面对错误输入或异常情况下的行为,确保能适当地处理这些情况(如抛出异常)。
- 性能测试:对于性能敏感的代码,测试其在不同规模输入下的表现,确保满足性能要求。
- 可维护性测试:确保方法在代码重构后仍然通过测试,维护其功能的完整性。
测试驱动开发(TDD)
测试驱动开发是一种开发过程,强调在编写代码之前先编写测试。具体步骤如下:
- 识别新特性:确定需要实现的新功能。
- 编写单元测试:为该功能编写测试代码,描述预期行为。
- 运行测试:执行测试,确认测试失败(因为尚未实现该功能)。
- 编写实现代码:编写代码以实现功能,使测试通过。
- 可选:重构代码:对代码进行重构,以提升性能和可读性,但要确保现有测试仍然通过。
虽然 TDD 在本课程中并非强制要求,单元测试却是一个良好的编程实践,可以帮助开发者在代码中保持信心。
集成测试
集成测试用于验证多个组件之间的交互是否正常。这种测试确保不同模块协同工作,确保系统的整体功能。
总结
编写测试是良好编程习惯的体现,只有在测试有意义时才进行测试。可选材料包括对测试驱动开发和单元测试的深入讨论,帮助理解其在现代软件开发中的重要性。