Python进阶(2) | py-sort源码浅析,TDD方式实现排序算法

Python进阶(2) | 排序算法的单元测试

文章目录

  • [Python进阶(2) | 排序算法的单元测试](#Python进阶(2) | 排序算法的单元测试)
    • [1. 目的](#1. 目的)
    • [2. 任务来源](#2. 任务来源)
    • [3. py-sorting 介绍](#3. py-sorting 介绍)
    • [4. 测试代码浅析](#4. 测试代码浅析)
      • [4.1 bubble_sort_test.py 源码分析](#4.1 bubble_sort_test.py 源码分析)
      • [4.2 BasePositiveIntegerSortTest 源码分析](#4.2 BasePositiveIntegerSortTest 源码分析)
    • [5. 模仿 BasePositiveIntegerSortTest 类: TDD 方式实现冒泡排序](#5. 模仿 BasePositiveIntegerSortTest 类: TDD 方式实现冒泡排序)
      • [5.1 第一个case: 空的输出](#5.1 第一个case: 空的输出)
      • [5.2 第二个case: 输入一个元素](#5.2 第二个case: 输入一个元素)
      • [5.3 测试小型的有序数组](#5.3 测试小型的有序数组)
      • [5.4 测试逆序有序的数组](#5.4 测试逆序有序的数组)
      • [5.5 补充测试用例](#5.5 补充测试用例)
    • [6. TDD 方式实现选择排序](#6. TDD 方式实现选择排序)
      • [6.1 重构代码: 复用测试用例](#6.1 重构代码: 复用测试用例)
      • [6.2 实现 selection sort 后再测试](#6.2 实现 selection sort 后再测试)
    • [7. TDD 方式实现 heap_sort](#7. TDD 方式实现 heap_sort)
    • [8. 总结](#8. 总结)
    • References

1. 目的

在数组元素排序这个任务上,使用 Python 编写单元测试, 并且进一步熟悉 VSCode 里的 Testing 界面的使用。

2. 任务来源

Python testing in Visual Studio Code 文档中提到, 可以查看 py-sorting 的代码, 它是一个包含了完整的代码测试代码的这里。

3. py-sorting 介绍

bash 复制代码
git clone https://github.com/gwtw/py-sorting

A collection of sorting algorithms written in Python.

py-sorting 仓库实现了多种排序算法,每一种排序算法的实现,都是一个单元。

先来看 sort 目录:

  • bubble_sort_optimised.py : 优化过的冒泡排序
  • bubble_sort.py : 冒泡排序
  • bucket_sort.py : 桶排序
  • cocktail_sort.py : 鸡尾酒排序
  • comb_sort.py : 不知道。
  • counting_sort.py : 计数排序?
  • gnome_sort.py: gnome 排序?
  • heapsort.py :堆排序
  • insertion_sort.py : 插入排序
  • merge_sort_bottom_up.py : 自底向上的归并排序
  • merge_sort.py : 归并排序
  • odd_even_sort.py : 奇数偶数排序
  • quicksort.py : 快排
  • radix_sort.py : 基数排序
  • selection_sort.py : 选择排序

4. 测试代码浅析

4.1 bubble_sort_test.py 源码分析

我们现在先忽略每个排序算法的具体实现。先看单元测试怎么写的。如果没有完备的测试, 很难保证功能代码的正确性。

挑选最简单的 bubble_sort_testt.py 来分析. 注释中 ## 开头的内容,是我增加的注释:

python 复制代码
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 类, 这是我们要测试的单元。这个类是继承了多个父类:

  • unittest.TestCase
  • BaseCustomComparisonSortTest
  • BasePositiveIntegerSortTest
  • BaseNegativeIntegerSortTest
  • BaseStringSortTest

我们用不同颜色来区分不同的分组,每种颜色是一个父类中的测试用例:

结合父类代码中分析,我们整理为如下要阅读的代码, 就比较直观了: self.sort 是一个成员, 并且是一个函数。子类 BubbleSort 中执行的赋值 self.sort = bubble_sort.sort, 相当于 C/C++ 中的函数函数指针, 其实就是回调函数:

python 复制代码
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([]))

4.2 BasePositiveIntegerSortTest 源码分析

base_positive_integer_sort_test.py 的代码不多,都是测试用例。我们逐个看下:

python 复制代码
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]))

5. 模仿 BasePositiveIntegerSortTest 类: TDD 方式实现冒泡排序

原版的 py-sort 实现的算法太多了,我这里仅仅考虑实现 bubble sort, 并且这个代码直接从 Copilot 生成即可, 主要还是练习 TDD 开发模式,以及单元测试、测试用例的编写。

5.1 第一个case: 空的输出

py-sort 的测试代码明显是经过重构的, 这样的代码虽然很鲁棒, 但对于不熟悉单元测试的人来说不够直观。我们在单个文件内给出冒泡排序的最简单的测试用例: 测试空的输入,预期结果也是空的:

python 复制代码
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 界面里执行测试,结果是通过的:

5.2 第二个case: 输入一个元素

原版 py-sort 并没有单独写这种 case, 但是最为 scratch 的单元测试编写, 先写这一个case并没有什么问题,后续如果有更加通用的测试用例能够覆盖当前用例, 可以重构的时候合并掉。新增的代码用 ## 做了标记。

python 复制代码
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 代码没有任何改动, 运行单元测试通过了:

5.3 测试小型的有序数组

python 复制代码
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 的实现则没有任何变化, 运行测试用例依然是成功的:

5.4 测试逆序有序的数组

新增如下测试代码:

python 复制代码
    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, 再跑测试,都通过了:

python 复制代码
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()

5.5 补充测试用例

个人认为完全的 TDD 还是很难的, 我们目前实现的 bubble_sort 其实已经正确了, 只不过我们保险起见, 或者说为了后续的其他排序, 可以进一步增加测试用例。

python 复制代码
    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]))

6. TDD 方式实现选择排序

基于第5步的测试用例,我们再实现一个新的排序算法:selection_sort。

6.1 重构代码: 复用测试用例

把原本 BubbleSortTest 类的测试用例代码, 拆到 BasePositiveNumberSortTest 类中, 然后让 BubbleSortTest 类继承自 BasePositiveNumberSortTest。此时和先前测试结果一样,这是第一次重构。

接下来是功能实现:添加 selection_sort 函数,添加 SelectionSortTest 类。都是模仿性质的代码。文件也改名为 test_sort.py:

python 复制代码
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()

不出意外的, 选择排序在逆序数组的测试用例上失败了:

6.2 实现 selection sort 后再测试

让 copilot 写出正确的选择排序后,测试通过了:

python 复制代码
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

7. TDD 方式实现 heap_sort

我们注意到 py-sort 源码代码, 每个sort函数中还额外传入了 compare 参数, 因此我们将5、6小节实现的函数做重构, 将 a > b 的比较改为 compare(a, b) > 0.

我们还注意到 py-sort 的大量测试用例,暂时先不管内容, 先无脑使用, 那么将我们的 my-sort 目录下原本放在 sort_test.py 中的测试用例,全都删掉,改为使用原版 py-test 的4个测试用例文件:

  • base_custom_comparison_sort_test.py
  • base_negative_integer_sort_test.py
  • base_positive_integer_sort_test.py
  • base_string_sort_test.py

此时单元测试变得非常简洁, 以 bubble_sort 为例:

python 复制代码
class BubbleSortTest(unittest.TestCase,
                     BaseCustomComparisonSortTest,
                     BasePositiveIntegerSortTest,
                     BaseNegativeIntegerSortTest,
                     BaseStringSortTest):
    def setUp(self):
        self.sort = bubble_sort

完整的代码如下, 包含了 heap_sort:

python 复制代码
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()

8. 总结

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 界面节省了自行挑选需要运行的单元测试的成本,很好用。

References

相关推荐
网易独家音乐人Mike Zhou3 小时前
【卡尔曼滤波】数据预测Prediction观测器的理论推导及应用 C语言、Python实现(Kalman Filter)
c语言·python·单片机·物联网·算法·嵌入式·iot
安静读书3 小时前
Python解析视频FPS(帧率)、分辨率信息
python·opencv·音视频
小二·5 小时前
java基础面试题笔记(基础篇)
java·笔记·python
小喵要摸鱼6 小时前
Python 神经网络项目常用语法
python
一念之坤7 小时前
零基础学Python之数据结构 -- 01篇
数据结构·python
wxl7812278 小时前
如何使用本地大模型做数据分析
python·数据挖掘·数据分析·代码解释器
NoneCoder8 小时前
Python入门(12)--数据处理
开发语言·python
LKID体9 小时前
Python操作neo4j库py2neo使用(一)
python·oracle·neo4j
小尤笔记9 小时前
利用Python编写简单登录系统
开发语言·python·数据分析·python基础
FreedomLeo19 小时前
Python数据分析NumPy和pandas(四十、Python 中的建模库statsmodels 和 scikit-learn)
python·机器学习·数据分析·scikit-learn·statsmodels·numpy和pandas