在数组元素排序这个任务上,使用 Python 编写单元测试, 并且进一步熟悉 VSCode 里的 Testing 界面的使用。
在 Python testing in Visual Studio Code 文档中提到, 可以查看 py-sorting 的代码, 它是一个包含了完整的代码测试代码的这里。
git clone https://github.com/gwtw/py-sorting
A collection of sorting algorithms written in Python.
py-sorting 仓库实现了多种排序算法,每一种排序算法的实现,都是一个单元。
我们现在先忽略每个排序算法的具体实现。先看单元测试怎么写的。如果没有完备的测试, 很难保证功能代码的正确性。
挑选最简单的 bubble_sort_testt.py
来分析. 注释中 ##
开头的内容,是我增加的注释:
import unittest ## 使用 Python 标准库里的 unitest 框架来编写单元测试
import os ## 引入 os 模块
import sys ## 引入 sys 模块
## 考虑到了复用性, 基础的几种排序侧测试,已经在别的地方写好了,现在引入
from base_custom_comparison_sort_test import BaseCustomComparisonSortTest ## 引入基础的定制比较排序测试。
from base_positive_integer_sort_test import BasePositiveIntegerSortTest ## 引入正整数的排序测试
from base_negative_integer_sort_test import BaseNegativeIntegerSortTest ## 引入负整数的排序测试
from base_string_sort_test import BaseStringSortTest ## 引入基础字符串排序
sys.path.append(os.path.join(os.path.dirname(__file__), os.pardir, 'sort')) ## 把 sort 目录放入搜索目录
import bubble_sort ## 引入 sort 目录里的 bubble_sort 模块
## 开始编写正式的测试代码,是一个 class, 继承自 unitttest.TestCase 类, 传入了上面导入的几种排序测试类
class BubbleSortTest(unittest.TestCase,
BaseCustomComparisonSortTest,
BasePositiveIntegerSortTest,
BaseNegativeIntegerSortTest,
BaseStringSortTest):
## 使用2空格缩进
## 定义 setUp 函数: 这个函数在每次单元测试被执行的时候, 是第一句被执行的内容。
def setUp(self):
self.sort = bubble_sort.sort
if __name__ == '__main__':
unittest.main()
上述代码让人看的很晕, 其实主要内容是, 定义了一个 BubbleSortTest 类, 这是我们要测试的单元。这个类是继承了多个父类:
我们用不同颜色来区分不同的分组,每种颜色是一个父类中的测试用例:
结合父类代码中分析,我们整理为如下要阅读的代码, 就比较直观了: self.sort
是一个成员, 并且是一个函数。子类 BubbleSort
中执行的赋值 self.sort = bubble_sort.sort
, 相当于 C/C++ 中的函数函数指针, 其实就是回调函数:
class BasePositiveIntegerSortTest(object):
def test_sorts_empty_array(self):
self.assertEqual([], self.sort([]))
class BubbleSortTest(unittest.TestCase,
BaseCustomComparisonSortTest,
BasePositiveIntegerSortTest,
BaseNegativeIntegerSortTest,
BaseStringSortTest):
def setUp(self):
self.sort = bubble_sort.sort
# 子类自动继承了父类的函数,相当于这里有如下代码:
# def test_sorts_empty_array(self):
# self.assertEqual([], self.sort([]))
base_positive_integer_sort_test.py 的代码不多,都是测试用例。我们逐个看下:
class BasePositiveIntegerSortTest(object):
# 测试空的数组,排序结果也应该是空的
def test_sorts_empty_array(self):
self.assertEqual([], self.sort([]))
# 测试小型的有序数组,排序后应该和输入一样
# 其实这个测试代码写得不够好。应该避免重复写,可以提取为变量。
def test_sorts_small_sorted_array(self):
self.assertEqual([1,2,3,4,5], self.sort([1,2,3,4,5]))
# 测试小型的逆序数组
def test_sorts_small_reverse_sorted_array(self):
self.assertEqual([1,2,3,4,5], self.sort([5,4,3,2,1]))
# 测试小型的部分有序的数组
def test_sorts_small_sorted_array_with_two_values_swapped(self):
self.assertEqual([1,2,3,4,5], self.sort([1,2,5,4,3]))
# 测试大型的有序数组
def test_sorts_large_sorted_array(self):
self.assertEqual(
[0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20],
self.sort([0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20]))
# 测试大型的逆序数组
def test_sorts_large_reverse_sorted_array(self):
self.assertEqual(
[0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20],
self.sort([20,19,18,17,16,15,14,13,12,11,10,9,8,7,6,5,4,3,2,1,0]))
# 测试大型的大部分有序的数组
def test_sorts_large_sorted_array_with_two_values_swapped(self):
self.assertEqual(
[0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20],
self.sort([0,1,2,8,4,5,6,7,3,9,10,11,12,13,14,15,16,17,18,19,20]))
原版的 py-sort 实现的算法太多了,我这里仅仅考虑实现 bubble sort, 并且这个代码直接从 Copilot 生成即可, 主要还是练习 TDD 开发模式,以及单元测试、测试用例的编写。
py-sort 的测试代码明显是经过重构的, 这样的代码虽然很鲁棒, 但对于不熟悉单元测试的人来说不够直观。我们在单个文件内给出冒泡排序的最简单的测试用例: 测试空的输入,预期结果也是空的:
import unittest
def bubble_sort(arr):
return arr
class BubbleSortTest(unittest.TestCase):
def setUp(self):
self.sort = bubble_sort
def test_sorts_empty_array(self):
self.assertEqual([], self.sort([]))
if __name__ == "__main__":
unittest.main()
没错,这里的冒泡排序,是原样返回输入。这在测试用例仅仅测试空输入的情况下,结果是正确的(也是空的列表)。
在 VSCode Testing 界面里执行测试,结果是通过的:
原版 py-sort 并没有单独写这种 case, 但是最为 scratch 的单元测试编写, 先写这一个case并没有什么问题,后续如果有更加通用的测试用例能够覆盖当前用例, 可以重构的时候合并掉。新增的代码用 ##
做了标记。
import unittest
def bubble_sort(arr):
return arr
class BubbleSortTest(unittest.TestCase):
def setUp(self):
self.sort = bubble_sort
def test_sorts_empty_array(self):
self.assertEqual([], self.sort([]))
def test_sorts_single_element_array(self): ##
self.assertEqual([1], self.sort([1])) ##
if __name__ == "__main__":
unittest.main()
bubble_sort 代码没有任何改动, 运行单元测试通过了:
import unittest
def bubble_sort(arr):
return arr
class BubbleSortTest(unittest.TestCase):
def setUp(self):
self.sort = bubble_sort
def test_sorts_empty_array(self):
self.assertEqual([], self.sort([]))
def test_sorts_single_element_array(self):
self.assertEqual([1], self.sort([1]))
def test_sorts_small_sorted_array(self): ##
arr = [1,2,3,4,5] ##
self.assertEqual(arr, self.sort(arr)) ##
if __name__ == "__main__":
unittest.main()
增加的测试代码很简单, bubble_sort 的实现则没有任何变化, 运行测试用例依然是成功的:
新增如下测试代码:
def test_sorts_small_reverse_sorted_array(self):
arr = [5,4,3,2,1]
self.assertEqual([1,2,3,4,5], self.sort(arr))
这次触发了测试失败, 终于不再那么“无聊”:
这次我们循规蹈矩的实现 bubble_sort, 再跑测试,都通过了:
import unittest
def bubble_sort(arr : list):
n = len(arr)
for i in range(n):
for j in range(n-i-1):
if arr[j] > arr[j+1]:
arr[j],arr[j+1] = arr[j+1],arr[j]
return arr
class BubbleSortTest(unittest.TestCase):
def setUp(self):
self.sort = bubble_sort
def test_sorts_empty_array(self):
self.assertEqual([], self.sort([]))
def test_sorts_single_element_array(self):
self.assertEqual([1], self.sort([1]))
def test_sorts_small_sorted_array(self):
arr = [1,2,3,4,5]
self.assertEqual(arr, self.sort(arr))
def test_sorts_small_reverse_sorted_array(self):
arr = [5,4,3,2,1]
self.assertEqual([1,2,3,4,5], self.sort(arr))
if __name__ == "__main__":
unittest.main()
个人认为完全的 TDD 还是很难的, 我们目前实现的 bubble_sort 其实已经正确了, 只不过我们保险起见, 或者说为了后续的其他排序, 可以进一步增加测试用例。
def test_sorts_small_sorted_array_with_two_values_swapped(self):
self.assertEqual([1,2,3,4,5], self.sort([1,2,5,4,3]))
基于第5步的测试用例,我们再实现一个新的排序算法:selection_sort。
把原本 BubbleSortTest 类的测试用例代码, 拆到 BasePositiveNumberSortTest 类中, 然后让 BubbleSortTest 类继承自 BasePositiveNumberSortTest。此时和先前测试结果一样,这是第一次重构。
接下来是功能实现:添加 selection_sort 函数,添加 SelectionSortTest 类。都是模仿性质的代码。文件也改名为 test_sort.py:
import unittest
def bubble_sort(arr : list):
n = len(arr)
for i in range(n):
for j in range(n-i-1):
if arr[j] > arr[j+1]:
arr[j],arr[j+1] = arr[j+1],arr[j]
return arr
def selection_sort(arr : list):
return arr
class BasePositiveNumberSortTest(object):
def test_sorts_empty_array(self):
self.assertEqual([], self.sort([]))
def test_sorts_single_element_array(self):
self.assertEqual([1], self.sort([1]))
def test_sorts_small_sorted_array(self):
arr = [1,2,3,4,5]
self.assertEqual(arr, self.sort(arr))
def test_sorts_small_reverse_sorted_array(self):
arr = [5,4,3,2,1]
self.assertEqual([1,2,3,4,5], self.sort(arr))
def test_sorts_small_sorted_array_with_two_values_swapped(self):
self.assertEqual([1,2,3,4,5], self.sort([1,2,5,4,3]))
class BubbleSortTest(unittest.TestCase, BasePositiveNumberSortTest):
def setUp(self):
self.sort = bubble_sort
class SelectionSortTest(unittest.TestCase, BasePositiveNumberSortTest):
def setUp(self):
self.sort = selection_sort
if __name__ == "__main__":
unittest.main()
让 copilot 写出正确的选择排序后,测试通过了:
def selection_sort(arr : list):
n = len(arr)
for i in range(n):
min_index = i
for j in range(i+1, n):
if arr[j] < arr[min_index]:
min_index = j
arr[i],arr[min_index] = arr[min_index],arr[i]
return arr
我们注意到 py-sort 源码代码, 每个sort函数中还额外传入了 compare 参数, 因此我们将5、6小节实现的函数做重构, 将 a > b
的比较改为 compare(a, b) > 0
.
我们还注意到 py-sort 的大量测试用例,暂时先不管内容, 先无脑使用, 那么将我们的 my-sort 目录下原本放在 sort_test.py 中的测试用例,全都删掉,改为使用原版 py-test 的4个测试用例文件:
此时单元测试变得非常简洁, 以 bubble_sort 为例:
class BubbleSortTest(unittest.TestCase,
BaseCustomComparisonSortTest,
BasePositiveIntegerSortTest,
BaseNegativeIntegerSortTest,
BaseStringSortTest):
def setUp(self):
self.sort = bubble_sort
完整的代码如下, 包含了 heap_sort:
import unittest
from base_custom_comparison_sort_test import BaseCustomComparisonSortTest
from base_positive_integer_sort_test import BasePositiveIntegerSortTest
from base_negative_integer_sort_test import BaseNegativeIntegerSortTest
from base_string_sort_test import BaseStringSortTest
def default_compare(a, b):
if a < b:
return -1
elif a > b:
return 1
return 0
def bubble_sort(arr : list, compare=default_compare):
n = len(arr)
for i in range(n):
for j in range(n-i-1):
if compare(arr[j], arr[j+1]) > 0:
arr[j],arr[j+1] = arr[j+1],arr[j]
return arr
def selection_sort(arr : list, compare=default_compare):
n = len(arr)
for i in range(n):
min_index = i
for j in range(i+1, n):
if compare(arr[j], arr[min_index]) < 0:
min_index = j
arr[i],arr[min_index] = arr[min_index],arr[i]
return arr
# implementation of heap sort
def heap_sort(arr : list, compare=default_compare):
n = len(arr)
for i in range(n//2-1, -1, -1):
heapify(arr, n, i, compare)
for i in range(n-1, 0, -1):
arr[i], arr[0] = arr[0], arr[i]
heapify(arr, i, 0, compare)
return arr
def heapify(arr : list, n : int, i : int, compare):
largest = i
left = 2 * i + 1
right = 2 * i + 2
if left < n and compare(arr[left], arr[largest]) > 0:
largest = left
if right < n and compare(arr[right], arr[largest]) > 0:
largest = right
if largest != i:
arr[i],arr[largest] = arr[largest],arr[i]
heapify(arr, n, largest, compare)
class BubbleSortTest(unittest.TestCase,
BaseCustomComparisonSortTest,
BasePositiveIntegerSortTest,
BaseNegativeIntegerSortTest,
BaseStringSortTest):
def setUp(self):
self.sort = bubble_sort
class SelectionSortTest(unittest.TestCase,
BaseCustomComparisonSortTest,
BasePositiveIntegerSortTest,
BaseNegativeIntegerSortTest,
BaseStringSortTest):
def setUp(self):
self.sort = selection_sort
class HeapSortTest(unittest.TestCase,
BaseCustomComparisonSortTest,
BasePositiveIntegerSortTest,
BaseNegativeIntegerSortTest,
BaseStringSortTest):
def setUp(self):
self.sort = heap_sort
if __name__ == "__main__":
unittest.main()
py-sort 这个仓库是 VSCode 官方指定的用来学习 Python 单元测试的仓库, 里面的单元测试代码中, 核心语句是 self.assertEqual()。
py-sort 的代码是经过明显重构的, 核心的 self.assertEqual() 被在多个test函数中调用, 这些 test 函数分布在4个class中,每个 class分别测试某一系列的case。
通过继承这4个测试class,以及继承 unittest.TesetCase 类, 子类可以几乎不写代码,仅仅给 self.sort 这个 callback 函数赋值, 在执行单元测试的时候会自动执行父类中的 test_xxx()
函数。
通过先写单元测试, 或者说不是一上来就写功能代码, 而是先把能想到的测试必须要通过的输入输出写出来, 能够提发现没有通过的case。
TDD 也许并不是最佳方式, 因为一开始可能没有想到所有 case, 但随着功能开发的推进, 可以补充测试用例, 边补充边重新测试, 也能较早的为最终正确的结果提供一定保障。
最后的最后, VSCode 的 Testing 界面节省了自行挑选需要运行的单元测试的成本,很好用。