Python 代码测试与调试全解析
1. 调试与测试概述
调试是消除程序中问题的过程,这本身就极具挑战性,而更难的是首先找到问题所在。随着程序规模的增大,其使用方式会增多,需要检查的潜在问题点也会相应增加。例如,一个文字处理器,它有样式、页面布局、文件格式等多种选项,每个选项都可能隐藏着 bug,甚至不同选项的组合也可能出现问题,像特定字体与特定布局搭配使用时可能就会有问题。
2. 单元测试基础
单元测试是测试程序最基本的形式,它主要是针对一小段代码进行测试,确保其按预期运行,常用于检查单个方法和函数是否正常工作。单元测试的本质是使用一组特定的输入运行代码,并检查输出是否正确。
2.1 示例函数及手动测试
以一个将字符串转换为大写的函数为例:
def capitalise(input_string):
output_string = ""
for character in input_string:
if character.isupper():
output_string = output_string + character
else:
output_string = output_string + chr(ord(character)-32)
return output_string
print(capitalise("helloWorld"))
这里利用了 Python 的 UTF - 8 字符编码,大写字母在 ASCII 码表中比小写字母小 32。我们可以手动打印输出结果来检查代码是否正常工作。
2.2 使用 unittest 模块进行自动化测试
import unittest
def capitalise(input_string):
output_string = ""
for character in input_string:
if character.isupper():
output_string = output_string + character
else:
output_string = output_string + chr(ord(character)-32)
return output_string
class Tests(unittest.TestCase):
def test_1(self):
self.assertEqual("HELLOWORLD", capitalise("helloWorld"))
if __name__ == '__main__':
unittest.main()
当运行
unittest.main()
时,Python 会运行
unittest.TestCase
子类中所有以
test_
开头的方法。在这个例子中,只有
test_1
方法。使用单元测试的真正优势在于可以组合多个测试用例,快速检查程序是否正常工作。
2.3 增加测试用例及问题发现
我们可以增加更多测试用例,如检查包含空格的字符串、特殊字符、数字等情况:
class Tests(unittest.TestCase):
def test_1(self):
self.assertEqual("HELLOWORLD", capitalise("helloWorld"))
def test_2(self):
self.assertEqual("HELLO WORLD", capitalise("Hello World"))
def test_3(self):
self.assertEqual('!"£$%^&*()_+-=',
capitalise('!"£$%^&*()_+-='))
def test_4(self):
self.assertEqual("1234567890", capitalise("1234567890"))
def test_5(self):
self.assertEqual("HELLO WORLD", capitalise("HELLO WORLD"))
def test_6(self):
self.assertEqual("`¬#~'@;:,.<>/?",
capitalise("`¬#~'@;:,.<>/?"))
运行这些测试用例时,会发现大部分测试都失败了。问题出在程序逻辑上,原代码是对非大写字符进行处理,而我们期望的是只对小写字符进行转换,其他字符保持不变。
2.4 修复代码
将
capitalise
函数修改如下:
def capitalise(input_string):
output_string = ""
for character in input_string:
if character.islower():
output_string = output_string + chr(ord(character)-32)
else:
output_string = output_string + character
return output_string
修改后再运行测试用例,所有测试应该都能通过。
3. unittest 输出详细程度设置
默认情况下,
unittest
只有在一个或多个测试失败时才会给出详细信息,否则只返回一个整体的 “OK”。可以通过两种方式设置输出的详细程度:
-
命令行方式
:在运行脚本时添加
-v
标志,如
python3 capitalise.py -v
。
-
代码内设置
:将代码中的
unittest.main()
改为
unittest.main(verbosity = 2)
。
4. 更多断言方法
在单元测试中,除了
self.assertEqual()
外,还有许多其他的断言方法:
| 断言方法 | 功能 |
| ---- | ---- |
|
assertSequencesEqual(sequence1, sequence2)
| 检查两个序列是否相同 |
|
assertListEqual(list1, list2)
| 检查两个列表是否相同 |
|
assertTupleEqual(tuple1, tuple2)
| 检查两个元组是否相同 |
|
assertSetEqual(set1, set2)
| 检查两个集合是否相同 |
|
assertDictEqual(dict1, dict2)
| 检查两个字典是否相同 |
|
assertIn(value, structure)
| 检查值是否在结构中 |
|
assertNotIn(value, structure)
| 检查值是否不在结构中 |
|
assertMultiLineEqual(string1, string2)
| 检查两个多行字符串是否相同 |
|
assertNotEqual(value1, value2)
| 检查两个值是否不相等 |
|
assertGreater(value1, value2)
| 检查 value1 是否大于 value2 |
|
assertGreaterEqual(value1, value2)
| 检查 value1 是否大于等于 value2 |
|
assertLess(value1, value2)
| 检查 value1 是否小于 value2 |
|
assertLessEqual(value1, value2)
| 检查 value1 是否小于等于 value2 |
|
assertAlmostEqual(value1, value2)
| 检查两个值的差值是否小于 0.000001 |
|
assertNotAlmostEqual(value1, value2)
| 检查两个值的差值是否不小于 0.000001 |
|
assertTrue(value)
| 检查值是否为 True |
|
assertFalse(value)
| 检查值是否为 False |
每个
test_
方法应该有一个断言方法调用,用于确定该测试的成功或失败。单元测试可以用于测试驱动开发(先写测试用例,再根据测试用例编写代码),不过大多数程序员会在开发后期编写测试用例,以确保程序正常工作。
5. 测试套件用于回归测试
编写程序通常不是一次性完成的,在开发过程中会不断添加新功能、修复 bug。但添加新功能时可能会破坏原有的正常功能,因此需要进行回归测试,即对旧代码进行测试。随着程序变大,测试用例增多,每次都运行所有测试可能不现实,这时可以将测试用例分组到不同的测试套件中。
if __name__ == '__main__':
letters_suite = unittest.TestSuite()
symbols_suite = unittest.TestSuite()
letters_suite.addTest(Tests("test_1"))
symbols_suite.addTest(Tests("test_2"))
symbols_suite.addTest(Tests("test_3"))
symbols_suite.addTest(Tests("test_4"))
symbols_suite.addTest(Tests("test_5"))
symbols_suite.addTest(Tests("test_6"))
all_suite = unittest.TestSuite()
all_suite.addTest(letters_suite)
all_suite.addTest(symbols_suite)
unittest.TextTestRunner(verbosity=2).run(all_suite)
这里将测试用例分为了
letters_suite
(测试字母相关)、
symbols_suite
(测试符号相关)和
all_suite
(包含所有测试)三个测试套件,可以根据需要运行不同的套件。
6. 整体测试与用户测试
单元测试虽然可以自动化且能快速检查代码是否正常,但它不能覆盖所有情况。即使每个部分单独测试都正常,整体组合时仍可能出现问题。在商业软件开发中,单元测试完成后,代码会交给质量保证团队进行整体测试。他们会列出一系列测试用例,涵盖软件的各种使用场景,通过手动或专业测试软件模拟用户操作来进行测试。
对于个人开发者来说,虽然可能没有质量保证团队,但可以借鉴专业方法。在测试前,列出程序的所有功能,确定测试输入和预期输出,然后逐一检查。虽然每次代码更改后都进行全面测试可能过于繁琐,但定期进行,尤其是在重大版本发布前进行是很有必要的。
此外,用户测试也很重要。开发者对自己的软件了如指掌,但用户可能并不了解。软件需要帮助用户理解其功能并提供足够的信息,让用户知道如何操作。理想情况下,可以找一群人来进行用户测试,但这通常很难实现。比较可行的方法是倾听用户的使用反馈。
7. 测试程度的把握
有句关于软件 bug 的老话:“没有证据不代表不存在”。无论对软件进行多少测试,都无法证明它完全没有 bug,实际上几乎不可能写出没有任何 bug 的软件。测试的目的不是打造完美的软件,而是让软件达到“足够好”的程度。不同项目对“足够好”的定义不同,软件越重要,就越需要进行更多的测试,但所有软件都至少应该进行一些测试。虽然测试不如实现新功能那样吸引人,但确保少数功能经过充分测试往往比有大量有 bug 的功能更重要。
总之,编程应该是有趣的,将问题分解成小步骤,通常就能轻松编写代码。如果你在编程过程中遇到困难,可以参考 Python 官方文档。只要找到感兴趣的领域并深入探索,即使是树莓派这样的小型设备,也能实现很多功能。
Python 代码测试与调试全解析(续)
8. 测试流程总结
为了更清晰地展示整个测试过程,我们可以用一个流程图来概括:
graph LR
A[编写代码] --> B[添加调试信息]
B --> C[进行单元测试]
C --> D{单元测试是否通过}
D -- 是 --> E[分组到测试套件]
D -- 否 --> F[修复代码]
F --> C
E --> G[进行回归测试]
G --> H{回归测试是否通过}
H -- 是 --> I[进行整体测试]
H -- 否 --> F
I --> J{整体测试是否通过}
J -- 是 --> K[进行用户测试]
J -- 否 --> F
K --> L{用户测试是否满意}
L -- 是 --> M[发布软件]
L -- 否 --> F
这个流程图展示了从代码编写到软件发布的整个测试流程。首先编写代码,添加调试信息帮助定位问题。然后进行单元测试,若单元测试不通过则修复代码,直到通过为止。接着将测试用例分组到测试套件进行回归测试,确保添加新功能时不破坏原有功能。回归测试通过后进行整体测试,检查各个部分组合在一起时是否正常工作。最后进行用户测试,根据用户反馈进一步优化代码,直到用户满意后发布软件。
9. 调试技巧总结
在调试过程中,除了使用
print()
语句输出调试信息外,还有一些其他的技巧:
-
使用条件调试
:可以设置一个调试变量,通过改变变量的值来控制调试信息的输出。例如:
debug = True
user_input = "example"
choices = {"example": "This is an example"}
if debug:
print("DEBUG type(user_input): ", type(user_input))
for key in choices.keys():
print("DEBUG type(key): ", type(key), "key: ", key)
if user_input in choices.keys():
print("You chose", choices[user_input])
else:
print("Unknown choice")
这样在调试完成后,只需将
debug
变量设置为
False
,就可以关闭调试信息的输出。
-
使用日志模块
:Python 的
logging
模块可以更灵活地控制调试信息的输出。它可以设置不同的日志级别,如
DEBUG
、
INFO
、
WARNING
、
ERROR
等,根据需要输出不同级别的信息。例如:
import logging
logging.basicConfig(level=logging.DEBUG)
user_input = "example"
choices = {"example": "This is an example"}
logging.debug("type(user_input): %s", type(user_input))
for key in choices.keys():
logging.debug("type(key): %s, key: %s", type(key), key)
if user_input in choices.keys():
print("You chose", choices[user_input])
else:
print("Unknown choice")
使用日志模块可以更好地管理调试信息,方便在不同环境下进行调试。
10. 测试的重要性再强调
测试在软件开发过程中起着至关重要的作用,具体体现在以下几个方面:
| 重要性 | 说明 |
| ---- | ---- |
| 发现问题 | 可以帮助我们发现代码中的 bug,确保软件的正确性。例如在前面的
capitalise
函数中,通过增加测试用例发现了程序逻辑的问题。 |
| 保证质量 | 经过充分测试的软件,其质量更有保障,能够减少用户在使用过程中遇到问题的概率。 |
| 提高可维护性 | 良好的测试用例可以作为代码的文档,帮助后续开发者理解代码的功能和预期行为,提高代码的可维护性。 |
| 降低成本 | 在开发早期发现并解决问题,比在软件发布后修复问题的成本要低得多。例如,如果在单元测试阶段发现问题,只需要修改一小段代码;但如果在软件发布后才发现问题,可能需要对整个系统进行排查和修复。 |
11. 总结与展望
通过前面的介绍,我们了解了 Python 代码测试与调试的相关知识,包括调试的基本概念、单元测试的方法、测试套件的使用、整体测试和用户测试的重要性,以及如何把握测试的程度。在实际开发中,我们应该养成良好的测试习惯,将测试融入到开发的每一个环节。
虽然我们已经介绍了很多测试方法,但测试是一个不断发展的领域,还有许多高级的测试技术和工具等待我们去探索。例如,模拟测试可以在不依赖外部资源的情况下对代码进行测试;性能测试可以检查软件在不同负载下的性能表现;安全测试可以发现软件中的安全漏洞等。
希望大家在学习和使用 Python 编程的过程中,能够充分利用测试和调试技术,编写出高质量、稳定可靠的软件。同时,不要忘记编程的乐趣,不断探索和创新,挖掘 Python 的无限潜力。无论你是初学者还是有经验的开发者,都可以通过不断学习和实践,提升自己的编程能力。相信在未来的编程之路上,你一定能够取得更好的成绩。
Python测试与调试全攻略
超级会员免费看

被折叠的 条评论
为什么被折叠?



