单元测试基础
学习单元测试的基础知识。
从无工具开始
我们先通过一个纯 JavaScript 的函数单元测试示例来入门。
这里只使用代码,不依赖任何工具。
我们测试的函数是一个简单的 flatMap 实现(参考 MDN 文档)。
我们将设计四个测试用例,确保我们的实现符合标准行为。
测试用例 1
1var arr = [1, 2, 3, 4]; 2 3arr.flatMap((x) => [x, x * 2]); 4// [1, 2, 2, 4, 3, 6, 4, 8]
该代码接收一个数字数组,并对每个元素应用映射函数 x => [x, x * 2]。这会生成一个数组的数组:[[1, 2], [2, 4], [3, 6], [4, 8]],然后将其扁平化为单层数组。
预期结果是 [1, 2, 2, 4, 3, 6, 4, 8]。
测试用例 2
1let arr1 = ["it's Sunny in", '', 'California always']; 2 3arr1.map((x) => x.split(' ')); 4// [['it's','Sunny','in'],[''],['California', 'always']] 5 6arr1.flatMap((x) => x.split(' ')); 7// ['it's','Sunny','in', '', 'California', 'always']
该代码接收一个字符串数组,对每个字符串应用 x.split(" ") 映射函数。这会产生不同长度的子数组组成的数组:[["it's","Sunny","in"],[""],["California", "always"]],然后进行扁平化处理。
预期结果是 ["it's","Sunny","in", "", "California", "always"]。
测试用例 3
1let arr1 = [1, 2, 3, 4]; 2 3arr1.map((x) => [x * 2]); 4// [[2], [4], [6], [8]]
该代码接收一个数字数组,对每个元素应用 x => [x * 2] 映射函数,生成一个包含单个元素的数组的数组:[[2], [4], [6], [8]],然后将其扁平化为单层数组。
预期结果是 [2, 4, 6, 8]。
测试用例 4
1arr1.flatMap((x) => [x * 2]); 2// [2, 4, 6, 8] 3 4// 仅扁平化一层 5arr1.flatMap((x) => [[x * 2]]); 6// [[2], [4], [6], [8]]
该代码接收一个数字数组,对每个元素应用 x => [[x * 2]] 映射函数,生成一个嵌套了多层数组的结构:[[[2]], [[4]], [[6]], [[8]]],然后进行扁平化处理。
预期结果是 [[2], [4], [6], [8]],这验证了 flatMap 只会将一层嵌套的数组展开。
1// 2Array.prototype.flatMap = function(cb) { 3 return []; 4 // 返回 this.map(cb).reduce((acc, n) => acc.concat(n), []); 5} 6 7 8 9// 测试用例 1 10const numbers = [1, 2, 3, 4]; 11 12const actual = numbers.flatMap(x => [x, x * 2]); 13const expected = '1,2,2,4,3,6,4,8'; 14console.log('测试用例 1 通过:', actual.join() === expected); 15 16 17 18// 测试用例 2 19const strings = ["it's Sunny in", "", "California"]; 20 21const actual1 = strings.flatMap(x => x.split(" ")); 22const expected1 = ["it's", "Sunny", "in", "", "California"]; 23console.log('测试用例 2 通过:', actual1.join() === expected1.join()); 24 25 26 27// 测试用例 3 28const numbers2 = [1, 2, 3, 4]; 29const actual2 = numbers2.flatMap(x => [x * 2]); 30const expected2 = [2, 4, 6, 8]; 31console.log('测试用例 3 通过:', actual2.join() === expected2.join()); 32 33// 测试用例 4 34// 仅扁平化一层 35console.log('测试用例 4 通过:', numbers.flatMap(x => [[x * 2]]).join() === [[2], [4], [6], [8]].join());
我们最初这样实现函数:
1Array.prototype.flatMap = function (cb) { 2 return []; 3};
此时运行程序,输出如下:
1测试用例 1 通过: false 2测试用例 2 通过: false 3测试用例 3 通过: false 4测试用例 4 通过: false
这是因为我们尚未真正实现该函数,它只是返回一个空数组。
尝试自行实现该函数,或取消第 4 行注释,将第 3 行替换为该行代码,重新运行。
此时应输出:
1测试用例 1 通过: true 2测试用例 2 通过: true 3测试用例 3 通过: true 4测试用例 4 通过: true
测试方法
- 获取函数的实际输出结果(actual)。
- 将其与预期结果(expected)进行比较。
- 根据是否匹配,判断测试用例是否通过。
详细分析
- 我们从创建一个 flatMap 的兼容性实现开始。
- 然后编写一个占位实现,始终返回空数组
[]。
测试用例 1:
-
验证
flatMap能正确处理包含多个元素的数组。 -
准备输入数据
numbers。 -
执行函数并获取实际结果:
1const numbers = [1, 2, 3, 4]; 2const actual = numbers.flatMap(x => [x, x * 2]); -
准备预期结果:
1const expected = '1,2,2,4,3,6,4,8'; 2console.log('测试用例 1 通过:', actual.join() === expected); -
输出实际结果与预期结果的比对结果。
测试用例 2:
-
验证
flatMap能正确处理字符串类型的数据。 -
准备输入数据:
const strings = ["it's Sunny in", "", "California"];。 -
获取实际结果:
1const actual1 = strings.flatMap(x => x.split(' ')); -
准备预期结果:
1const expected1 = ["it's", "Sunny", "in", "", "California"]; -
比较
actual和expected并输出结果:1console.log('测试用例 2 通过:', actual1.join() === expected1.join());
测试用例 3:
此用例与测试用例 1 非常相似,唯一的区别是回调函数生成的是长度为 1 的数组。
测试用例 4:
-
验证
flatMap能正确处理深层嵌套的数组,并且仅展开一层。 -
在一行中完成实际结果与预期结果的比较:
1console.log('测试用例 4 通过:', numbers.flatMap(x => [[x * 2]]).join() === [[2], [4], [6], [8]].join());
其中,actual 是 numbers.flatMap(x => [[x * 2]]),复用了第一个例子中的 numbers;expected 是 [[2], [4], [6], [8]].join()。
我们已经完成测试了吗?
这些测试确保了四个具体场景能够正常工作。虽然还有许多其他可能的使用场景,但我们无需全部覆盖。
只要保证核心使用场景有充分的测试覆盖即可。如果后续发现代码中存在缺陷,我们可以新增针对性的测试用例来捕获该问题。因此,测试本身永远不会真正“完成”。
这就是测试的本质:它们随着代码库一起持续演进和变化!