20、Python 代码测试与调试全解析

Python测试与调试全攻略

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 的无限潜力。无论你是初学者还是有经验的开发者,都可以通过不断学习和实践,提升自己的编程能力。相信在未来的编程之路上,你一定能够取得更好的成绩。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符  | 博主筛选后可见
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值