Python进阶(1) | 使用VScode写单元测试

Python进阶(1) | 单元测试

2024.01.28

VSCode: 1.85.1

Linux(ubuntu 22.04)

文章目录

  • [Python进阶(1) | 单元测试](#Python进阶(1) | 单元测试)
    • [1. 目的](#1. 目的)
    • [2. Python Profile](#2. Python Profile)
    • [3. 单元测试框架](#3. 单元测试框架)
      • [3.1 什么是单元测试](#3.1 什么是单元测试)
      • [3.2 选一个单元测试框架](#3.2 选一个单元测试框架)
      • [3.3 编写 Python 单元测试代码](#3.3 编写 Python 单元测试代码)
      • [3.4 在 VSCode 里发现单元测试](#3.4 在 VSCode 里发现单元测试)
      • [3.5 再写一个单元和测试: IoU 的计算](#3.5 再写一个单元和测试: IoU 的计算)
    • [4. 总结](#4. 总结)
    • [5. References](#5. References)

1. 目的

使用 Python 实现一些小工具、库的时候,增加单元测试来保证正确性。

重读 VSCode 的 Python 官方文档, 更新个人的 Python 开发效率。

2. Python Profile

VSCode 提供了定制 profile 的功能, 个人目前理解为类似于 vim/emacs 里的模式的升级版。以前我只是配置VSCode的全局配置和当前工程配置, 而 Profile 则是建立了不同的配置,每个打开的VSCode工程都可以在不同的 profile 之间切换。

举例: 分别设置 C++ Profile 和 Python profile, 在 Python profile 和 C++ profile 中使用不同的快捷键、不同的UI布局等。

关于 profile 的完整文档在 https://code.visualstudio.com/docs/editor/profiles

官方提供了 Python 的profile,可以根据这个预定义的 profile, 继承它,创建一个自己的 Python profile:

https://code.visualstudio.com/docs/editor/profiles#_python-profile-template

3. 单元测试框架

3.1 什么是单元测试

A unit is a specific piece of code to be tested, such as a function or a class. Unit tests are then other pieces of code that specifically exercise the code unit with a full range of different inputs, including boundary and edge cases. Both the unittest and pytest frameworks can be used to write unit tests.

所谓单元,指的是一段特定的要被测试的代码,比如说一个函数、一个类。

所谓测试,指的是被测试代码A之外的代码B, 也就是说B这部分代码存在的意义,就是测试A这部分代码。

测试代码通常需要包含各种不同的输入,包括边界情况。

单元测试仅仅关注输入 和 输出, 不关注代码实现的细节。

因此,所谓单元测试,首先需要划分出单元,然后针对每个单元(或者仅对于关注的单元),编写测试代码。

For each input, you then define the function's expected return value (or values).

对于被测试的代码的每一种输入,你需要定义它的预期结果。

With all the arguments and expected return values in hand, you now write the tests themselves, which are pieces of code that call the function with a particular input, then compare the actual return value with the expected return value (this comparison is called an assertion):

然后调用被测试的代码A: 给它传入输入, 获得它的输出结果, 并且和你预设的结果进行比对,结果一样则成功,不一样则报告失败。

https://code.visualstudio.com/docs/python/testing

3.2 选一个单元测试框架

Python 最常用的单元测试框架: unittest 和 pytest.

unittest 是 Python 标准库的模块, 也就是 Python 安装后自带的。 pytest 则需要自行安装: pip install pytest.

3.3 编写 Python 单元测试代码

首先,是被测试的单元的代码, inc_dec.py:

python 复制代码
def increment(x: int):
    return x + 1

def decrement(x: int):
    return x - 1

然后, 是编写测试代码. 先用 unittest 写一遍:test_unittest.py

python 复制代码
import inc_dec
import unittest

class Test_TestIncrementDecrement(unittest.TestCase):
    def test_increment(self):
        self.assertEqual(inc_dec.increment(3), 4)
    
    # 这个测试用例一定会失败,是刻意做的
    def test_decrement(self):
        self.assertEqual(inc_dec.decrement(3), 4)

if __name__ == '__main__':
    unittest.main()

再用 pytest 写一遍, 写法更简单:

python 复制代码
import inc_dec

def test_increment():
    assert inc_dec.increment(3) == 4

# 这个测试用例一定会失败,是刻意做的
def test_decrement():
    assert inc_dec.decrement(3) == 4

3.4 在 VSCode 里发现单元测试

首先在 VSCode 里点击左侧的 Testing 按钮, 创建测试相关的配置:

它对应到 .vscode/setting.json 里的内容:

json 复制代码
{
    "python.testing.pytestArgs": [
        "."
    ],
    "python.testing.unittestEnabled": false,
    "python.testing.pytestEnabled": true
}

然后点击 Testing 视图中的测试用例中最上方的按钮, 会自动发现和执行所有的测试用例:

在 Testing 界面中点击到 "失败" (红色) 的case, 会看到失败的具体测试代码。我们发现是测试代码本身写错, 于是改掉, 然后重新在 Testing 界面中执行测试:

最终,我们看到 Testing 界面中的每一项都是绿色, 表示都成功了:

3.5 再写一个单元和测试: IoU 的计算

前面给出的 inc_dec.py 的代码太简单, 测试代码也不太符合预期解决的问题。

单元测试的预期目的,是发现单元中的bug。这次写一个经典的计算两个Box的IoU的函数,并且故意缺少处理非法box长度的情况。

bbox.py:

python 复制代码
# define Box class
class Box(object):
    def __init__(self, x, y, w, h, score):
        self.x = x
        self.y = y
        self.w = w
        self.h = h
        self.score = score
    def __repr__(self):
        return 'Box(x=%f, y=%f, w=%f, h=%f, score=%f)' % (self.x, self.y, self.w, self.h, self.score)

# calculate IoU of two boxes
def box_iou(box1, box2):
    # get coordinates of intersecting rectangle
    x_left = max(box1.x, box2.x)
    y_top = max(box1.y, box2.y)
    x_right = min(box1.x + box1.w, box2.x + box2.w)
    y_bottom = min(box1.y + box1.h, box2.y + box2.h)
    # if x_right < x_left or y_bottom < y_top:
    #     return 0.0
    # intersection area
    intersection_area = (x_right - x_left) * (y_bottom - y_top)
    # union area
    box1_area = box1.w * box1.h
    box2_area = box2.w * box2.h
    union_area = box1_area + box2_area - intersection_area
    return intersection_area / union_area

test_bbox.py:

python 复制代码
import bbox

def test_box_iou():
    box1 = bbox.Box(0, 0, 1, 1, 0.9)
    box2 = bbox.Box(0, 0, 1, 1, 0.9)
    assert bbox.box_iou(box1, box2) == 1.0

def test_box_iou2():
    box1 = bbox.Box(0, 0, 1, 1, 0.9)
    box2 = bbox.Box(1, 1, 2, 2, 0.9)
    assert bbox.box_iou(box1, box2) == 0

def test_box_iou3(): # 这个例子是边界case,很容易失败
    box1 = bbox.Box(0, 0, 0, 0, 0.9)
    box2 = bbox.Box(1, 1, 1, 1, 0.9)
    assert bbox.box_iou(box1, box2) == 0

上述代码在 test_box_iou3() 时失败了, 错误类型是出现了除0错误。显然,除非两个 box 大小都是0,否则不会出现除以0的情况。于是很偷懒的改了一下:

python 复制代码
# calculate IoU of two boxes
def box_iou(box1, box2):
    # get coordinates of intersecting rectangle
    x_left = max(box1.x, box2.x)
    y_top = max(box1.y, box2.y)
    x_right = min(box1.x + box1.w, box2.x + box2.w)
    y_bottom = min(box1.y + box1.h, box2.y + box2.h)
    # if x_right < x_left or y_bottom < y_top:
    #     return 0.0
    # intersection area
    intersection_area = (x_right - x_left) * (y_bottom - y_top)
    # union area
    box1_area = box1.w * box1.h
    box2_area = box2.w * box2.h
    union_area = box1_area + box2_area - intersection_area
    if union_area == 0:
        return 0
    return intersection_area / union_area

再增加一个侧测试用例:当box本身的宽度或高度为负值时,预期结果我们设置为0. 测试代码是:

python 复制代码
def test_box_iou4():
    box1 = bbox.Box(0, 0, -1, -1, 0.9)
    box2 = bbox.Box(0, 0, 2, 2, 0.9)
    iou = bbox.box_iou(box1, box2)
    assert iou == 0

IoU的实现代码,仍然是用很偷懒的修改:

python 复制代码
# calculate IoU of two boxes
def box_iou(box1, box2):
    # get coordinates of intersecting rectangle
    x_left = max(box1.x, box2.x)
    y_top = max(box1.y, box2.y)
    x_right = min(box1.x + box1.w, box2.x + box2.w)
    y_bottom = min(box1.y + box1.h, box2.y + box2.h)
    # if x_right < x_left or y_bottom < y_top:
    #     return 0.0
    # intersection area
    intersection_area = (x_right - x_left) * (y_bottom - y_top)
    # union area
    box1_area = box1.w * box1.h
    box2_area = box2.w * box2.h
    union_area = box1_area + box2_area - intersection_area
    # if union_area == 0:
    #     return 0
    # if box1.w < 0 or box1.h < 0 or box2.w < 0 or box2.h < 0:
    #     return 0
    return intersection_area / union_area

此时的测试仍然不够完备。再补充一个:

python 复制代码
def test_box_iou5():
    box1 = bbox.Box(0, 0, 0, 0, 0.9)
    box2 = bbox.Box(0, 0, 0, 0, 0.9)
    iou = bbox.box_iou(box1, box2)
    assert iou == 0

现在,把包含了补丁的 box_iou() 重构一番,得到:

python 复制代码
# calculate IoU of two boxes
def box_iou(box1, box2):
    # get coordinates of intersecting rectangle
    x_left = max(box1.x, box2.x)
    y_top = max(box1.y, box2.y)
    x_right = min(box1.x + box1.w, box2.x + box2.w)
    y_bottom = min(box1.y + box1.h, box2.y + box2.h)
    if x_right <= x_left or y_bottom <= y_top:
        return 0.0
    # intersection area
    intersection_area = (x_right - x_left) * (y_bottom - y_top)
    # union area
    box1_area = box1.w * box1.h
    box2_area = box2.w * box2.h
    union_area = box1_area + box2_area - intersection_area
    return intersection_area / union_area

4. 总结

VSCode 的 Testing 视图,改善了运行单元测试的交互界面。传统的 C/C++ 中, gtest 框架通过传入 --gtest_filter=xxx 来过滤测试, 在 VSCode 面前仍然落后。

至于单元测试代码是否够好, 一个标准是覆盖率的高低, 就像 IoU 的例子, 第一次用 ChatGPT 生成代码时,虽然看似正确, 但其实 test_box_iou5() 这个测试用例(两个box的大小都是0,并且重合)是无法通过的。

因此, VSCode 的 Testing 界面仅仅是锦上添花, 单元测试的编写仍然需要考虑周全。

5. References

相关推荐
2301_8038756120 分钟前
CSS如何制作导航栏平滑移动_使用transition与left属性
jvm·数据库·python
2501_933329555 小时前
媒介宣发技术实践:Infoseek舆情系统的AI中台架构与应用解析
开发语言·人工智能·架构·数据库开发
[J] 一坚6 小时前
嵌入式高手C
c语言·开发语言·stm32·单片机·mcu·51单片机·iot
odoo中国6 小时前
Odoo 19技术教程 : 如何在 Odoo 19 中创建 Many2one 组件
开发语言·odoo·odoo19·odoo技术·many2one
逻辑驱动的ken6 小时前
Java高频面试考点场景题14
java·开发语言·深度学习·面试·职场和发展·求职招聘·春招
茅盾体6 小时前
汽车零件订单自动同步系统方案
python
2401_883600256 小时前
golang如何理解weak pointer弱引用_golang weak pointer弱引用总结
jvm·数据库·python
FreakStudio6 小时前
和做工厂系统的印尼老哥,复刻了一套属于 MicroPython 的包管理系统
python·单片机·嵌入式·大学生·面向对象·并行计算·电子diy·电子计算机
2301_773553626 小时前
mysql如何评估SQL语句的索引开销_mysql性能追踪与分析
jvm·数据库·python
pele7 小时前
PHP源码运行受主板供电影响吗_供电相数重要性说明【技巧】
jvm·数据库·python