☰
python编程入门到实战 第11章测试代码
2026/10/31 2:00:09 网站建设 项目流程

在前面几个章节里, 我们已经会写函数了, 会创建类了, 也会处理文件了, 而且能够把一个小程序慢慢拼凑起来了, 但是程序能写出来, 不代表程序就一定可靠。

很多初学者都有过一种非常常见的经历, 他们的代码当时能运行, 可是过一会儿改了几行之后, 原来正常的地方又坏了。还有另一种情况是自己的以为某个函数已经写对了, 结果换了一组输入数据之后才发现输出并不符合预期。

所以, 真正能够使程序变得稳定的关键因素, 绝不仅仅是“具备编写代码的能力”, 此外还必须学会对代码进行验证操作, 而测试的核心价值正是体现在这一层面之中, 它的作用目的并非是为了给大家人为地增添额外的麻烦事端, 而是要通过这种方式帮助大家提前发现潜在存在的各类问题, 进而从而最大程度地减少那些由于缺少自动化检查而导致的、必须靠人工去反复鼠标点击来进行的低效且耗时巨大的检查工作流程所带来的负面影响。

这一章节, 主要是带你去认识那个东西里面的自动化测试思路。你会看的很清楚的一点是, 测试这个东西, 并不是只有专家才会用的一种很高大上的工具。

对于那些刚入门的初学者人员来说, 这个测试更像是一份可以重复执行的检查清单。我们只需要在函数或者类拥有了明确的运行目标之后, 就可以把希望程序表现出的样子写成测试代码, 然后让那个环境帮我们去进行反复的检查。

11.1 为什么要测试代码

有很多的人, 在头一回听到“测试”这个词儿的时候, 会心里想着, 这事情得等到那个项目做得很大了以后, 才要去考虑。事实, 其实正正好是反过来的。越是处于刚开始学习的那个阶段, 就越应当把那种要进行测试的意识, 早点儿给养成了。

道理就在于说, 在这个时候咱们写出来的那些代码, 它还显得不怎么稳当, 而且改动起来的次数也要更多一些。要是没有一个固定的、用来检查的规矩或者办法存在着的活, 那么就会有一种非常容易发生的情况出现, 就是改着改着就把原来本来是正确的功能, 给损坏掉了。

测试的直接价值, 说白了大概有三个方面的事情。第一个方面就是去确认一个函数或者类, 它是不是真的把之前自己说过的任务给实实在在完成了。第二个方面是当你动了一些旧代码之后, 可以用它来快速检查一遍, 看看是不是因为修改而搞出了什么新出来的问题没有。

第三个方面就是它确实会让你被迫去思考得更清楚一些, 到底这个函数应该怎么去做才对, 原因其实也很简单, 就是因为很多时候如果你写不出测试语句来的话就说明你自己对这个目标的定义本身就还存在着不够清晰不明确的地方。

你可以把测试这个事, 理解成是给程序准备好的一套标准问答内容。当程序每次都运行到这个地方来的时侯, 都要接受同样的检查任务。只要最终得到的结果, 是不符合当初设定的预期情况的话, 测试这一机制就会立刻提醒你注意相关的问题出现。

本节小结

测试这个东西, 它根本不是那种可有可无的多余步骤, 它是那种能够帮着你把程序的稳定状态给保持住的这样一种能力, 同时也是能够让你自己在以后的工作中减少返工次数的一种基础能力。

11.这个测试是可以通过的, 大家在做的时候就可以顺利通过。

在学习进行测试的时候, 最佳的一个出发点, 其实并不是先去研究那些复杂的语法现象, 而是应该先去观察一个已经成功通过的案例。这是因为你如果率先看到了通过时的状态具体是个什么样子, 那么后续再在面对失败的提示信息时, 内心就不会感到惊慌失措了。

假设说我们写了一个函数, 这个函数是用来对姓名进行整理, 把它弄成更适合显示出来的样子。比如说是把janis 这个字符串, 把它变成Janis。那么这样子的一个功能, 它看起来是特别简单的一种操作。但是它实际上也已经足够用来演示测试方面的基本思路了。

示例代码

def get_formatted_name(first, last):

full_name = f"{first} {last}"

return full_name.title()

逐行解释

在第 1 行定义了一个函数。这个函数使用了 `()`。它接收了两个参数。第一个参数的名字是 `first`。第二个参数的姓氏是 `last`。

把名字和姓氏拼接起来, 中间加上空格, 弄成了一个完整的姓名, 把这个结果保存到了变量里面。

在第3行的位置调用函数`title()`, 该操作的目的是对姓名里面的每一个单词进行整理, 具体做法是让这些单词的第一个字母变成大写形式, 最后把这个处理完之后的结果给返回出去。

现在你如果去手动的调用一下这个函数, 它确实是能够正常工作的, 可是手动试一次这个东西, 并不意味着在未来的时间里它会一直维持正确的状态不发生错误, 所以我们要继续去编写测试代码。

测试代码

import unittest

from name_function import get_formatted_name

class NamesTestCase(unittest.TestCase):

def test_first_last_name(self):

formatted_name = get_formatted_name("janis", "joplin")

self.assertEqual(formatted_name, "Janis Joplin")

if __name__ == "__main__":

unittest.main()

逐行解释

在第 1 行, 你会看到对 `` 模块进行了导入的操作。关于这个模块的功能情况, 它是属于标准库里面一部分的一个存在, 而且它的主要作用呢, 是用来编写测试代码的, 就是这么一个专门用于写测试方面的工具。

第 3 行是从文件里头导入咱前面已经写好好的函数, 用的是代码中的引用方式。

第 6 行位置, 它定义了一个用来做测试用的类, 该类是处于省略状态下的那个父类的后代, 从继承关系上来进行解释说明的话, 这意味着说在这个类里面所编写的代码内容, 实际上是一组标准化的用于执行的测试用例集合。

在第7行这个地方是定义了一个测试方法的, 并且这个方法是括号空的括号。因为这个方法的名字是有下划线接着test的, 这种开头方式大家觉得是一个规矩。如果测度工具看到这样的名字的话, 就会认为这是需要进行运行执行的一个测试项目。

第 8 行调用目标函数,把结果保存到 ``。

第九行的时候, 是使用`self.()`这个方式来去做断言的, 它的这个意思就是说, 我是希望说, 实际的这个结果跟预期的那个结果是相等的, 如果是相等的情况下, 就代表着测试通过了, 如果不相等的话, 那就代表测试失败了。

在第12行这个位置, 它会对当前的文件是不是作为主程序来运行这件事情进行一个判断的动作。

第 13 行执行了这个 .main() 方法, 然后由这个方法负责去自动查找, 并且还负责去运行这个文件里面所有的测试代码。

运行出来的最终结果。

当函数输出正确, 运行测试之后, 一般就能看到一个表示成功的结果。针对初学者来说, 最关键的方面并不是去记住那个界面本身看起来是什么样子, 而是要能够弄明白这样一个事实情况就是: 如果测试通过成功了, 这说明当前你自己设定的预期目标和程序实际产生的行为之间是完全一致的。

新手提醒

很多初学者在头一回进行编写测试这一工作时, 往往习惯把主要精力集中在怎么样能够把所编写的这个测试用例顺利运行的这个事情上面, 与此同时却很容易忽略掉一个更加重要且核心的问题, 也就是说你需要去确认那个预期结果写得到底准不准确, 鉴于测试这项活动的根本性质在于它能够明确表达你所提出的各项要求, 因此判断断言部分描述得是否清晰明白, 其重要程度远远高于去纠结具体的语法结构是否正确无误。

本节小结

通过了一项测试, 这说明你已经掌握了测试的基本操作结构, 这种结构包括将模块进行导入, 创建专门的测试类, 编写用于检查的测试方法, 以及使用断言来确认结果是否符合预期。

11.第3项是那些没有办法顺利做完的检测, 结果都是通不过的。

光看测试通过了这一事实本身是不够的, 你必须要去清楚当测试出现失败的情况时这具体代表的是什么含义。之所以这么做的原因是, 那些真正具备价值的关键点, 在很大程度上根本不是在于测试结果变成了让人高兴的绿色状态。

真正的核心价值在于能够弄清楚测试报告为什么变红了从而出现了红色的标记。失败的提示信息这一点并不构成什么巨大的困扰。事实上失败的提示是一种信号, 它在向你表明程序当前的实际执行行为和你自己内心设定的预期之间存在着一定的差距或者偏差。

比如说, 在眼下这个时候我们想要让那个姓名的函数去处理像“中间名”这种类别的状况。假设有这样一个完整的姓名 ` 《《》` , 那么它的组成部分就不只只有两样东西了。要是之前那个函数依然只接收两个参数, 这样的话那些旧的代码就不够使用了。

示例代码

def get_formatted_name(first, last, middle=""):

if middle:

full_name = f"{first} {middle} {last}"

else:

full_name = f"{first} {last}"

return full_name.title()

逐行解释

在第1行当中, 给参数设置了默认值""。这就意味着中间名不用必须填写。

在第 2 行的时候, 会对 `` 这个对象或者字段是否包含着有效内容进行一个判断操作。

如果出现有中间名的情况的话, 那么就就去执行第3行, 把名字这部分和中间名这部分还有姓氏这三部分给拼凑在一起组成三个块。

如果不存在中间名这一情况, 那就去执行第 5 行的那个动作。这时候只把名字和姓氏拼合在一起处理。

到第 6 行这一步, 还是照样去把刚才整理好的那个完整的姓名给转成那种标题的样子, 然后再把它给返回回来。

在接下来这个阶段里面, 我们要去为一个叫做带着中间名的这样一个具体的情况, 然后额外增加一个全新的测试项目。

测试代码

import unittest

from name_function import get_formatted_name

class NamesTestCase(unittest.TestCase):

def test_first_last_name(self):

formatted_name = get_formatted_name("janis", "joplin")

self.assertEqual(formatted_name, "Janis Joplin")

def test_first_last_middle_name(self):

formatted_name = get_formatted_name("wolfgang", "mozart", "amadeus")

self.assertEqual(formatted_name, "Wolfgang Amadeus Mozart")

if __name__ == "__main__":

unittest.main()

逐行解释

前半部分的结构, 与上一个测试文件相比来说是完全一致的, 重点需要关注的是那些被新增的测试方法。

()`。

在调用这个方法的函数的时候, 大家多传入了一个参数, 这个参数的意思是指中间名, 而这个中间名的值则是空字符串, 也就是两个双引号里面的内容为空。

在断言这一块儿, 大家心里头盼着的、最后要拿在手里的结果嘛, 其实是那个叫` `的东西。

如果函数在现在这个状况下, 已经是能够直接去支持中间名的那种情况了, 那么, 这个新测试内容, 就会和前一个测试一起, 全部都通过。

只要那个函数还没有被改好, 测试出来的结果就会说得很清楚, 指明失败的地方到底是发生在哪一个测试方法里面的。

新手提醒

测试失败这种情况, 其实并不是说就一定不好, 相反地它最重要的一个功能表现是什么呢? 它是能够让你去把那个程序里面到底哪一部分地方没有达到你原本期望的那个样子给定位得更加清楚一些。对于那些刚开始学习的新手朋友来说, 这种做法会比自己去乱翻代码盲目地去查找错误还要更加有效并且省时间多了。

本节小结

那个没有通过测试的东西, 它并不是在否定你这个人本身, 它是在向你明确地传达一个信息, 那就是新的需求条件已经出现了, 所以你手里的旧代码必须跟着去完成升级的动作。

11.第4个部分是测试函数的内容。

当你已经会写一两个简单测试之后, 下一步需要建立一个更稳定的认识。函数测试的核心不是把函数跑一遍。而是对函数行为提出清晰要求。再用断言检查那些要求是不是成立了。

一个函数通常情况下, 我们是需要至少去考虑下面这几类相关的方面的问题的, 当输入是正常的状态的时候, 我们需要确认结果是否正确, 当输入是边界值的时候, 我们需要确认结果是否依然能够成立, 当参数是可选的时候, 我们需要确认默认的行为是否能够说得通,对于字符串、数字以及列表等等各种各样不同的类型的输入来说, 我们也需要确认程序到底有没有按照最初设计的逻辑进行处理。

在刚刚开始学习的那个阶段, 千万不要想着在一开始就把所有的测试情形全部都给它写满了, 但你必须得学会怎么去先抓住那些最最核心的几种场景。就拿姓名函数这个例子来说, 你起码要把两种主要的情况给覆盖住, 哪两种?

第一种是只包含名字和姓氏的状况, 第二种则是要同时存在名字、姓氏以及中间名的那些状况。这么做有一个很明显的好处, 那就是等到以后你对代码进行修改的时候, 你就能非常迅速地去检测出来, 你的改动是否已经把原本已经具备的功能给破坏掉了。

本节小结

在进行测试函数操作的时候, 其关键之处并不在于去努力地凑够测试用例的数量, 而是在于把最重要的、最常见的以及最容易出现错误的场景作为优先事项来予以覆盖。

11.5 测试类

先前进行的测试工作针对的是函数, 当下我们应当将视线转移并聚焦到类别的概念之上, 类往往比函数的结构更加难以把握, 原因在于它不仅仅具备方法的特征, 还会负责维持自身的状态数据, 所以, 当开展对类的测试任务时, 除了需要确认方法是否能够被执行外, 还需要仔细核对对象内部所存储的数据在经过一系列操作之后, 是不是变成了符合预期要求的正确状态。

这是一个很常见的例子, 情况是这样的, 假定我们编写了一个匿名调查类, 这个类的功能是收集用户的回答内容, 那么在开展测试工作的时候, 大家就不能仅仅是查看相关的方法是否已经被调用了, 还需要进一步去检查调用结束之后, 相应的答案数据是否确实地被保存到了列表对象里面。

示例代码

class AnonymousSurvey:

def __init__(self, question):

self.question = question

self.responses = []

def show_question(self):

print(self.question)

def store_response(self, new_response):

self.responses.append(new_response)

def show_results(self):

print("调查结果:")

for response in self.responses:

print(f"- {response}")

逐行解释

在第1行这里对类进行定义。

第 2 行到第 4 行是初始化方法。在创建对象的时候, 系统会把调查问题进行保存, 并且准备好一个空列表 `` , 这个列表是用来存放答案的。

在第6行到第7行这个地方, 是对()这个符号进行的定义, 其具体的作用, 也就是为了让调查的问题能够被显示出来。

第9行至第10行定义了“()”, 其作用在于将用户提交的新答案附加到这个列表之中。

在这段代码的第 12 行到第 15 行, 作者定义了一种叫做 `()` 的东西。并且在这里, 作者还在使用循环这种操作。作者要把所有被收集起来的答案, 一个一个地按顺序打印出来。

这个类的关键点不在显示文字,而在于:`()` 执行后,答案是否真的被保存到了 `` 里。

测试代码

import unittest

from survey import AnonymousSurvey

class TestAnonymousSurvey(unittest.TestCase):

def test_store_single_response(self):

question = "你最想练熟的 Python 技能是什么?

"

my_survey = AnonymousSurvey(question)

my_survey.store_response("函数")

self.assertIn("函数", my_survey.responses)

if __name__ == "__main__":

unittest.main()

逐行解释

在代码的第 1 行那儿, 把那个专门用来做测试的模块给导入进来。

在第3行这个位置的时候, 把那个你需要去进行测试的类导入进来。

第 6 行定义测试类。

第 7 行定义了一个用于测试的方法, 这个方法的主要功能是用来核查关于单条回答是否能够被进行正确保存这样的状况是否存在。

在第8行这里, 我们要准备好用于测试的问题内容。

在第 9 行那里, 你要针对那个提出的问题。去创建一个专门的调查对象, 让这个对象来负责应对这个问题情况。

在代码的第10行位置, 我们会去执行括号里的那个动作来把一条回答给好好地存住。

在第第 12 行那里使用了所谓的 self, 也就是用那种方式来发个断然声明去核对事情, 看看那个带引号的函数这两个字的这个答案, 它确确实实有没有在那个点符号所在的这列数据清单里真真正正地出现一回。

如果它存在于列表当中, 这就代表保存操作已经成功完成了;而如果它并不在列表之中,那就说明这个类所包含的方法并没有按照预期的那种方式来发挥作用。

本节小结

在运行那些测试程序类的时候, 我们需要注意去看一看具体的对象它的状态是不是发生了正确的改变, 而不是仅仅地观察某个方法有没有被执行过。

11.6 setUp() 的作用

随着测试的数量慢慢变多, 你会很快遇到一个问题, 那就是很多测试在开始之前都需要做准备工作, 而且这些工作都是重复的, 举个例子来说, 每一个测试都要先创建出同一个对象, 同时还要准备同一组基础数据, 要是每一个测试方法里面都重新写一遍的话, 不仅听起来很啰嗦, 而且在后面修改准备逻辑的时候也很容易出现漏掉没有改的情况。

在这个时候, 就可以去使用叫做`setUp()`这个东西。这个函数的具体作用就是说, 每一个进行运行的测试方法在执行那个主要过程之前, 它会自动先去把那段准备好的代码执行一遍。那么这样的话? 你就能够去把所有的公共准备部分的代码啊集中书写在一个特定的地方里面了。

示例代码

import unittest

from survey import AnonymousSurvey

class TestAnonymousSurvey(unittest.TestCase):

def setUp(self):

question = "你最想练熟的 Python 技能是什么?

"

self.my_survey = AnonymousSurvey(question)

self.responses = ["函数", "类", "文件处理"]

def test_store_single_response(self):

self.my_survey.store_response(self.responses[0])

self.assertIn(self.responses[0], self.my_survey.responses)

def test_store_three_responses(self):

for response in self.responses:

self.my_survey.store_response(response)

for response in self.responses:

self.assertIn(response, self.my_survey.responses)

if __name__ == "__main__":

unittest.main()

逐行解释

第 6 行定义测试类。

第 7 行定义了 setUp() 方法, 当测试框架准备执行一个测试方法的时候, 它会把这里的代码先自动运行一下。

在第 8 行这个地方, 大家是把调查的问题给进行准备好的。

在第 9 行的这个位置, 会把调查对象给创建出来, 然后再把这个东西保存到 `self.` 里面去。之所以在前面要加上 `self.` 这么一套东西, 它的目的就是要让测试类里面的其他的方法都能用到这个对象。

在代码的第 10 行这个地方, 程序员会弄出一套测试用的回答来, 然后呢, 把这些个东西给保存起来, 并且是保存到了一个叫 `self.` 的地方里面去。

从第12行开始, 进行第一个测试环节, 这个测试只用于验证单条回答的情况。

在第13行的位置, 我们把这个结果记录下来, 也就是把第一个回答给它保存下来。

第 14 行是去检查它, 看看这个元素是不是就已经在结果列表里面了。

从第 16 行开始进行第二个测试, 其最终目的, 是要去验证这样一个事实, 就是多条回答的存在, 也都是可以被保存下来的。

把第 17 行到第 18 行这一块拿来遍历准备好的回答列表, 然后呢, 就每一条回答都给它交给 `()`。

第20行到第21行, 再次遍历原始回答列表, 逐条进行检查, 看看每个回答是不是都已经被保存到 `self..` 里面了。

在第 24 行一直到第 25 行这一块地方, 主要就是去负责把这个测试的过程给启动起来。

新手提醒

`setUp()` 这个东西并不是非用不可的, 但是当出现这种情况, 就是说有好几个测试的方法, 它们都在做一样一样的准备步骤的时候, 这个 `setUp()` 就能让代码看起来更加清楚了。

它真正的好东西在哪里呢? 不是说“语法变得更高级了”, 主要的是能够减少重复性的工作, 并且还能降低后面进行修改的时候的成本。

本节小结

当存在多个测试, 并且这些测试都需要使用同样的初始化环境的时候, 建议大家使用的是 `setUp这个方法来进行统一准备, 这样做的话会比在每个单独的测试里重复进行创建对象的这一操作会更加地整洁。

11.7 本章小结

在这一章之中, 你完成了对自动化测试这一主题的首次系统性学习与接触。你已经充分明白了, 所谓测试环节, 它并非是没有必要的额外步骤, 而是为了确保程序的运行状态更加稳定而存在的核心工具, 这一点你已经非常清楚。

你也已经掌握了如何利用特定的技术手段去编写针对最基础的函数进行测试的代码以及针对类进行测试的代码, 同时也深入理解了断言这一概念在工作时所发挥的关键作用, 知道了当测试出现失败情况时那部分提示信息具有的价值所在, 此外你还认识了 `setUp()` 这个方法在减少重复执行准备工作方面所具有的用途。

从学习节奏的这个角度来看, 测试这一章是非常非常重要的。为什么它会这么关键? 因为以后的项目, 它一定会变得越来越长。同时, 代码文件也会越来越多。

如果没有建立起那种叫做测试的意识的话, 一旦碰到的问题变多了, 事情一多就乱套了。但是另一方面, 如果你真的学会了利用测试来帮忙检查自己的程序, 那么在往后写功能的时候, 心里头就会更踏实很多, 更有底一些。

在接下来往后继续学习的时候, 你就可以把参加测试这件事, 当成一个早就养成的习惯, 而不是把它看待成是那种需要额外花力气去干的任务。不管是在你写完了一个非常关键的函数之后, 还是在你补完了一个非常重要的类之后, 都要试着自己问自己一句: 我可不可以为它写一个最基本的那种测试?

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询