文章大纲
测试是编程工作的一部分,要调试就必须运行程序。
Python 不需要编译,所以测试就是运行程序。
先测试再编码
为程序的各个部分进行测似(单元测试)是非常重要的,必须时刻牢记:“测试一点点,再编写一点点代码”的理念。
测试在前,编码在后——测试驱动编程。
准确的需求说明
开发软件时,必须指导软件要解决的问题——要实现的目标。
通过编写需求说明来阐明程序的目标,需求说明也就是描述程序必须满足何种需求的文档,后续就可以方便的核实需求是否得到满足。
在 Python 中,可以将测试程序当作需求说明,先编写测试,再编写让测试通过的程序。
例如以下测试程序示例:
from area import rect_area
height = 3
width = 4
correct_answer = 12
answer = rect_area(height, width)
if correct_answer == answer:
print('Test Passed')
else:
print('Test Failed')
随后通过编写满足需求的 rect_area
函数来实现需求。
做好应对变化的准备
必须做好修改代码的心理准备,而不是固守既有代码,但修改会有风险。
如果程序设计良好(使用了合理的抽象和封装),修改带来的影响将是局部的,只会影响很小一段代码。
测试四部曲
测试驱动开发过程的各个阶段:
- 确定需要实现的功能
- 编写实现功能的框架代码,让程序能够运行
- 编写让测试刚好能通过的代码
- 改进(重构)代码以全面而准确的实现所需功能
测试工具
有多种测试模块可帮助自动测试过程:
unittest
:一个通用的测试框架doctest
:一个更简单的模块,为检查文档而设计,非常适合用于编写单元测试
doctest
基于文档实现测试的示例 my_math.py
:
def square(x):
'''
计算平方并返回结果
>>> square(2)
4
>>> square(3)
9
'''
return x * x
if __name__ == '__main__':
import doctest, my_math
doctest.testmod(my_math)
运行 python my_math.py -v
返回的内容:
Trying:
square(2)
Expecting:
4
ok
Trying:
square(3)
Expecting:
9
ok
1 items had no tests:
my_math
1 items passed all tests:
2 tests in my_math.square
2 tests in 2 items.
2 passed and 0 failed.
Test passed.
unittest
unittest
基于流行的 Java 测试框架 Junit
它更加灵活和强大。
使用模块 unitest
中的 TestCase
类编写一个测试:
import unittest, my_math
class ProductTestCase(unittest.TestCase):
def test_integers(self):
for x in range(-10, 10):
for y in range(-10, 10):
p = my_math.product(x,y)
self.assertEqual(p,x * y, 'Integer multiplication failed')
def test_floats(self):
for x in range(-10, 10):
for y in range(-10, 10):
x = x / 10
y = y / 10
p = my_math.product(x, y)
self.assertEqual(p, x * y, 'Float multiplication failed')
if __name__ == '__main__': unittest.main()
运行结果:
Ran 2 tests in 0.000s
OK
pytest
pytest
库是一组工具,可以帮助轻松的编写测试,而且能持续支持随项目增大而变得复杂的测试。
安装 pytest
Python 默认不包含 pytest
,需要通过第三方包进行安装:
pip3 install pytest
编写需要测试的代码
在 name_function.py
中编写 get_formatted_name
方法:
def get_formatted_name(first, last):
"""生成格式规范的名字
Args:
first (string): 名字
last (string): 姓
"""
full_name = f"{first} {last}"
return full_name.title()
编写一个使用该方法的程序 names.py
:
from name_function import get_formatted_name
print("Enter 'q' at any time to quit.")
while True:
first = input("\nPlease give me a first name: ")
if first == 'q': break
last = input("\nPlease give me a last name: ")
if last == 'q': break
formatted_name = get_formatted_name(first, last)
print(f"\t Neatly formatted name: {formatted_name}.")
编写测试
pytest
需要编写以 test_
开头的文件,当 pytest
运行时,它会查找以 test_
开头的文件,并运行其中的测试,且文件中的所需运行的函数也需定义以 test_
开头。
test_name_function.py
示例:
from name_function import get_formatted_name
def test_first_last_name():
formatted_name = get_formatted_name('janis', 'joplin')
assert formatted_name == 'Janis Joplin'
运行 pytest
命令进行测试:
(env) [hcai@P1-Gen4 Chapter_16]$ pytest
============== test session starts ===============
platform linux -- Python 3.12.1, pytest-7.4.4, pluggy-1.3.0
rootdir: /home/hcai/pythonProject/Chapter_16
collected 3 items
test_my_math.py .. [ 66%]
test_name_function.py . [100%]
=============== 3 passed in 0.01s ================
后续可以在该文件中添加新的测试函数,或在项目中添加新的测试文件。
超越单元测试
源代码检查是一种发现代码中常见错误或问题的方式。
性能分析指的是了解程序运行速度到底有多快。
满足 “使其管用,使其更好,使其更快” —— 古老规则。
使用 PyLint 检查源代码
安装 pylint
:
pip3 install pylint
使用 subprocess
调用外部检查器:
import unittest, my_math
from subprocess import Popen, PIPE
class ProductTestCase(unittest.TestCase):
def test_with_Pylint(self):
cmd = 'pylint', '-rn', 'my_math'
pylint = Popen(cmd, stdout=PIPE, stderr=PIPE)
self.assertEqual(pylint.stdout.read(), '')
if __name__ == '__main__': unittest.main()
也可以直接对文件进行检查:
pylint my_math.py
执行后显示的结果:
Your code has been rated at 6.25/10 (previous run: 2.86/10, +3.39)
性能分析
在编程中,不成熟的优化是万恶之源——C.A.R.Hoare.
标准库中包含一个性能分析模块 profile
,还有一个速度更快的版本 cProfile
,这个性能分析模块使用起来简单,只需要调用 run
方法并提供一个字符串参数。
>>> import cProfile
>>> from my_math import product
>>> cProfile.run('product(1,2)')
4 function calls in 0.000 seconds
Ordered by: standard name
ncalls tottime percall cumtime percall filename:lineno(function)
1 0.000 0.000 0.000 0.000 <string>:1(<module>)
1 0.000 0.000 0.000 0.000 my_math.py:6(product)
1 0.000 0.000 0.000 0.000 {built-in method builtins.exec}
1 0.000 0.000 0.000 0.000 {method 'disable' of '_lsprof.Profiler' objects}
将输出各个函数和方法被调用多少次以及执行花费的时间。
如果通过第二个参数向 run
提供一个文件名,分析结果将保存在该文件中,随后可以通过 pstats
模块来研究分析结果了。
>>> cProfile.run('product(1,2)', 'my_path.profile')
>>> import pstats
>>> p = pstats.Stats('my_path.profile')