第11章 测试代码

发布于 2022-04-01  490 次阅读


编写函数或类时,还可为其编写测试。通过测试,可确定代码面对各种输入都能够按要求的那样工作。测试让你深信,即便有更多人使用你的程序,它也能正确的工作。在程序中添加新代码时,也可以对其进行测试,确认不会破坏程序既有的行为。程序员都会犯错,因此每个程序员都必须经常测试其代码,在用户发现问题前找出它们。
在本章中,学习如何使用Python模块unittest中的工具来测试代码,还将编写测试用例。核实一系列输入都将得到预期的输出。将看到测试通过了是什么样子,测试未通过又是什么样子,还将知道测试未通过如何有助于改进代码。学习如何测试函数和类,并将知道为项目编写多少个测试。

11.1 测试函数

要学习测试,必须要有测试的代码。下面是一个简单的函数,它接受名和姓并返回整洁的姓名:

def get_formatted_name(first,last):
    """生成整洁的姓名"""
    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("\n Please give me a first name: ")
    if first == 'q':
        break
    last = input(" Please give me a last name: ")
    if last == 'q':
        break

    formatted_name = get_formatted_name(first,last)
    print(f"\tNeatky formatted name: {formatted_name}")

file

但是假如要加入一个中间名,不仅仅要修改函数还要修改该测试代码。
Python提供了一种能自动测试函数输出的高效方式。倘若对get_formatted_name()进行自动测试。就能始终确信当提供测试过的姓名时,该函数都能正确的工作。

11.1.1 单元测试和测试用例

Python标准库中的模块unittest提供了代码测试工具,单元测试用于核实函数的某个方面没有问题。
测试用例是一组单元测试,它们一道核实函数在各种情形下都符合要求。良好的测试用例考虑了函数可能收到的各种输入,包含针对所有的这些情形的测试,全覆盖的测试用例包含一整套单元测试,涵盖了各种可能的函数使用方式,对于大型项目,要进行全覆盖测试可能很难,通常,最初只要针对代码的重要行为编写测试即可,等项目被广泛使用时再考虑全覆盖。

11.1.2 可通过的测试

创建测试用例以后,再添加针对函数的单元测试就很简单了,要为函数编写测试用例,可先导入模块unittest和要测试的函数,再创建一个继承unittest.TestCase的类,并编写一系列方法对函数行为的不同方面进行测试。
下面的测试用例只包含一个方法,它检查函数get_formatted_name()在给定名和姓时能否正确工作:

import unittest
from name_function import get_formatted_name

class NameTestCase(unittest.TestCase):
    """测试name_function.py"""

    def test_first_last_name(self):
        """能够正确地处理像Guo an这样的姓名吗?"""
        formatted_name = get_formatted_name('Guo','an')
        self.assertEqual(formatted_name,'Guo An')

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

file
首先,导入模块unittest和要测试的函数get_formatted_name().创建了一个名为NameTestCase的类,用于包含一系列对get_formatted_name()的单元测试。这个类可以随意命名。这个类,必须继承unittest.TestCase类,这样Python才知道如何运行代码。
NameTestCase只包含一个方法,用于测试get_formatted_name()的一个方面,将该方法命名为test_first_last_name(),因为要核实的是只有名和姓能否被正确格式化。运行test_name_function.py时,所以有test_打头的方法都将自动运行。在这个方法中,调用了要测试的函数。
self.assertEqual(formatted_name,'Guo An')
处使用了unittest类最有用的功能之一:断言功方法,断言方法核实得到的结果是否与期望一直。为检查是否确实如此,调用unittest的方法assertEqual(),并向他传递formatted_name()和'Guo An'.意思如果相等,则ok,否则错误。
直接运行这个文件,但需要指出的是,很多测试框架都会先导入测试文件再运行。导入文件时,解释器将在导入的同时执行它。

if __name__ == '__main__'
# 处的if代码块检查特殊变量__name__,这个变量是再程序执行时设置的。
# 如果这个文件作为主程序执行,变量__name__将被设置为'__main__'.
# 在这里,调用unittest.main()来运行测试用例。
# 如果这个文件被测试框架导入,变量__name__的值将不是'__main__',因此不会调用unittest.main()

程序运行结果表面有一个测试通过了,消耗时间,,最后OK表明该测试用例中的所有单元测试都通过了。
上述输出表明,给定包含名和姓的姓名时,函数get_formatted_name()总是能够正确的处理。修改该函数后,可再此运行这个测试用例,如果它通过了,就表明给定给定Guo An这样的姓名时,改函数依然能够正确的处理。

11.1.3 未通过的测试

测试未通过时结果是什么样呢?
修改get_formatted_name()使其能够处理中间名,但同时故意让该函数无法正确处理像Guo An这样只有名和姓的名字。

def get_formatted_name(first,middle,last):
    """生成整洁的姓名"""
    full_name = f"{first} {middle} {last}"
    return full_name.title()

file
定位到NameTest中的test_first_last_name()导致了错误。
测试用例中包含众多单元测试时,知道哪个测试未通过至关重要。后面,看到一个标准的traceback,指出了get_formatted_name('Guo','an')有问题,因为缺少一个必不可少的位置实参。

11.1.4 测试未通过时怎么办

如果检查的条件没错,测试通过意味者函数的行为是对的,而测试未通过意味者编写的新代码有问题。因此,测试未通过时,不要修改测试,而应修复导致测试不能通过的代码。检查刚刚对函数所做的修改,找出导致函数行为不符合预期的修改。

def get_formatted_name(first,last,middle = None):
    """生成整洁的姓名"""
    if middle:
        full_name = f"{first} {middle} {last}"
    else:
        full_name = f"{first} {last}"
    return full_name.title()

现在,测试用例通过了,这意味着这个函数又能正确处理像Guo an这样的姓名了,而且暂时无需手工测试这个函数。这个函数之所以很容易修复,是因为未通过的测试让我们得知新代码破坏了原来的行为。

11.1.5 添加新测试

import unittest
from name_function import get_formatted_name

class NameTestCase(unittest.TestCase):
    """测试name_function.py"""

    def test_first_last_name(self):
        """能够正确地处理像Guo an这样的姓名吗?"""
        formatted_name = get_formatted_name('Guo','an')
        self.assertEqual(formatted_name,'Guo An')

    def test_first_last_middle(self):
        """能够正确处理Guo xiao an 这样的姓名吗?"""
        formatted_name = get_formatted_name('Guo','an','xiao')
        self.assertEqual(formatted_name,'Guo Xiao An')

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

file
可以在TestCase类中使用很长的方法名,而且这些方法名必须是描述性的,这样才能看懂测试未通过时的输出。这些方法由Python自动调用。

11.2 测试类

前面学习编写了针对单个函数的测试,下面编写针对类的测试。很多程序中都会用到类,因此证明你的类能够正确工作大有裨益。如果针对类的测试通过了,就能确信对类所作的改进没有意外地破话其原有的行为。

11.2.1 各种断言方法

Python在unittest.TestCase类中提供了很多断言方法。断言方法检查你认为的条件是否确实满足。如果该条件确实满足,你对程序行为的假设就得到了确认。可以确信其中没有错误。如果你认为应该满足的条件实际上并不满足,Python将引发异常。
下表描述了6个常用的断言方法,使用这些方法可核实法案会的值等于或不等于预期的值,返回的值为True或False,以及返回的值在列表中或不在列表中。只能在继承unittest.TestCase的类中使用这些方法,随后来看看如何在测试类时使用其中之一:

方法 用途
assertEqual(a,b) 核实a == b
assertNotEqual(a,b) 核实a != b
assertTrue(x) 核实x为True
assertFalse(x) 核实x为False
assertIn(item,list) 核实item在list中
assertNotIn(item,list) 核实item不在list中

11.2.2 一个要测试的类

类的测试与函数的测试相似。
下面编写一个要测试的类,来看一个帮助管理匿名调查的类:

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("Survey result:")
        for response in self.responses:
            print(f"- {response}")
from survey import AnonymousSurvey

# 定义一个问题,并创建一个调查
question = "What language did you first learn to speak?"
my_survey = AnonymousSurvey(question)

# 显示问题并存储答案
my_survey.show_question()
print("Enter 'q' at any time to quit.\n")
while True:
    response = input("Language: ")
    if response == 'q':
        break
    my_survey.store_response(response)

# 显示调查结果
print("\nThank you to everyone who participated in the survey!")
my_survey.show_results()

file
AnonymousSurvey类可用于进行简单的匿名调查,假设我们把它放在了模块survey中,并想进行改进:并每位用户都可输入多个答案;编写一个方法,只列出不同答案并指出每个答案出现了多少次;再编写一个类,用于管理非匿名调查。
进行上述修改存在风险,可能影响AnonymousSurvey类的当前行为。例如,允许每位用户输入多个答案时,可能会不小心修改处理单个答案的方式。要确认再开发这个模块时没有破坏既有行为,可以编写针对这个类的测试。

11.2.3 测试AnonymousSurvey类

下面来编写一个测试,对AnonymousSurvey类的行为的一个方面进行验证:如果用户面对调查值提供一个答案,这个答案也能被妥善地存储。为此,将在这个答案被存储后,使用方法assertIn()来核实它确实在答案列表中:

import unittest
from survey import AnonymousSurvey

class TestAnonymousSurvey(unittest.TestCase):
    """针对AnonymousSurvey类的测试"""
    def test_store_single_response(self):
        """测试单个答案会被妥善地存储"""
        question = "What language did you first learn to speak?"
        my_survey = AnonymousSurvey(question)
        my_survey.store_response('C')
        self.assertIn('C',my_survey.responses)

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

file

这很好,但只能收集一个答案的调查用途不大,下面来核实当用户提供三个答案时候,它们也能够被妥善的存储。为此,在TestAnonymousSurvey中再添加一个方法:

import unittest
from survey import AnonymousSurvey

class TestAnonymousSurvey(unittest.TestCase):
    """针对AnonymousSurvey类的测试"""
    def test_store_single_response(self):
        """测试单个答案会被妥善地存储"""
        question = "What language did you first learn to speak?"
        my_survey = AnonymousSurvey(question)
        my_survey.store_response('C')
        self.assertIn('C',my_survey.responses)

    def test_store_three_responses(self):
        """测试三个答案会被妥善的存储"""
        question = "What language did you first learn to speak?"
        my_survey = AnonymousSurvey(question)
        responses = ['C','Python','C++']
        for response in responses:
            my_survey.store_response(response)

        for response in responses:
            self.assertIn(response,my_survey.responses)

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

file

前述的做法的效果很好,但这些测试有些重复的地方。下面使用unittest的另一项功能来提高其效率。

11.2.4 方法setUp()

在前面的test_survey.py中,在每个测试方法中都创建了一个AnonymousSurvey实例,并在每个方法中创建了答案。unittest.TestCase类包含的方法setUp(),Python将先运行它,再运行各个以test_打头的方法。这样,再编写的每个测试方法中,都可以使用再方法setUp()中创建的对象。

import unittest
from survey import AnonymousSurvey

class TestAnonymousSurvey(unittest.TestCase):
    """针对AnonymousSurvey类的测试"""

    def setUp(self):
        """创建一个调查对象和一组答案,供使用的测试方法使用"""
        question = "What language did you first learn to speak?"
        self.my_survey = AnonymousSurvey(question)
        self.responses = ['C','Python','C++']
    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()

file
方法setUp()做了两件事情:创建一个调查对象,以及创建一个答案列表。存储这两样的东西的变量名包含前缀self(即存储在属性中),因此可在这个类的任何地方使用。这让两个测试方法都更简单,因为它们都不用创建调查对象和答案了。方法test_store_single_response()核实self.responses中的第一个答案self.responses[0]被妥善地存储,而方法test_store_three_response()核实self.response中的三个答案都被妥善的存储。
再此运行test_survey.py时,这两个测试也都通过了,如果要扩展AnonymousSurvey,使其允许每位用户输入多个答案,这些测试将很有用。修改代码以接受多个答案后,可运行这些测试,确认存储单个答案或一系列答案的行为未受影响。
测试自己编写的类时,方法setUp()让测试方法编写起来更容易;可在setUp()方法中创建一系列实例并设置其属性,再在测试方法中直接使用这些实例。相比于在每个测试方法中都创建实例并设置其属性,这要容易的多。

注意:运行测试用例时,每完成一个单元测试,Python都打印一个字符;测试通过实时打印一个句点,测试引发错误时打印一个E,而测试导致断言失败时则打印一个F。这就是你运行测试用例时,在输出的第一行中看到的句点和字符数量各不相同的原因。如果测试用例包含很多单元测试,需要运行很长时间,就可通过观察这些结果来获悉有多少个测试通过了。

11.3 小结

学习使用模块unittest中的工具来为函数和类编写测试;如何编写继承unittest.TestCase的类,以及如何编写测试方法,以核实函数和类的行为符合预期;
使用方法setUp()来根据类高效的创建实例并设置其属性,以便在类的所有测试方法中使用。
参与工作量较大的项目时,应该对自己所编写函数和类的重要行为进行测试。这样你就能更加确定自己所作的工作不会破坏项目的其他部分,从而自由地改进既有代码。如果不小心迫害了原有的功能,就会马上得知,从而更轻松的修复问题。
通过多开展测试来熟悉代码测试过程。
不要试图在项目早期试图编写全覆盖的测试用例,除非有充分的理由。


擦肩而过的概率