Python 测试代码|Python 主题月

608 阅读7分钟

本文正在参加「Python主题月」,详情查看 活动链接

编写函数或类时,还可为其编写测试。通过测试,可确定代码面对各种输入都能够按要求的那样工作。本次将学习如何使用 Python 模块 unittest 中的工具来测试代码,还将学习编写测试用例,核实一系列输入都将得到预期的输出。你将学习如何测试函数和类,并将知道该为项目编写多少个测试。

测试函数

  • 要学习测试,必须要有测试代码。以下是一个简单函数,它接受名和姓并返回整洁的姓名:
def get_formatted_name(first, last):
    """生成整洁的姓名。"""

    full_name = f"{first} {last}"
    return full_name.title()
  • 为了核实该函数的作用,编写一个使用该函数的程序。
from name_fucntion import get_formatted_name

print("Enter 'q' at any time to quit.")

while True:
    first = input(f"Please give me a first name: ")
    if first == 'q':
        break

    last = input(f"Please give me a last name: ")
    if last == 'q':
        break

    formatted_name = get_formatted_name(first, last)
    print(f"Neatly formatted name: {formatted_name}.")
  • 如果修改了该函数用于保证能够处理中间名,那么修改后就要进行测试。
  • 为此,每次修改了该函数都需要运行程序进行测试,十分麻烦。
  • 所幸,Python 提供了一种自动测试函数输出的高效方式。

单元测试和测试用例

  • Python 标准库中的模块 unittest 提供了代码测试工具。
  • 单元测试用于核实函数的某个方面没有问题。
  • 测试用例是一组单元测试,它们一道核实函数在各种情形下的行为都符合要求。

可通过的测试

  • 要为函数编写测试用例,可先导入模块 unittest要测试的函数,再创建一个继承 unittest.TestCase 的类,并编写一系列方法对函数行为的不同方面进行测试。
import unittest
from name_fucntion import get_formatted_name

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

    def test_first_last_name(self):
        """能够正确处理像 Janis Joplin 这样的姓名吗?"""
        formatted_name = get_formatted_name('janis', 'joplin')
        self.assertEqual(formatted_name, 'Janis Joplin')

if __name__ == '__main__':
    unittest.main()
  • 代码中使用了 unittest 类最有用的功能之一:断言方法。
  • 断言方法核实得到的结果是否与期望的结果一致。
  • 输出结果如下:
.
----------------------------------------------------------------------
Ran 1 test in 0.000s

OK
  • 第一行的句点表明有一个测试通过了。接下来的一行指出 Python 运行了一个测试,消耗的时间不到 0.001 秒。最后的 OK 表明该测试用例中的所有单元测试都通过了。

未通过的测试

  • 修改 get_formatted_name(),使其能够处理中间名,但同时故意让该函数无法正确处理像 Janis Joplin 这样只有名和姓的姓名。
def get_formatted_name(first, middle, last):
    """生成整洁的姓名。"""

    full_name = f"{first} {middle} {last}"
    return full_name.title()
  • 这个版本应该能够正确处理包含中间名的姓名,但对其进行测试时,我们发现它不再能正确处理只有名和姓的姓名。
E
======================================================================
ERROR: test_first_last_name (__main__.NamesTestCase)
能够正确处理像 Janis Joplin 这样的姓名吗?
----------------------------------------------------------------------
Traceback (most recent call last):
  File "test_name_function.py", line 9, in test_first_last_name
    formatted_name = get_formatted_name('janis', 'joplin')
TypeError: get_formatted_name() missing 1 required positional argument: 'last'

----------------------------------------------------------------------
Ran 1 test in 0.001s

FAILED (errors=1)
  • 第一行输出只有一个字母 E,指出测试用例中有一个单元测试导致了错误。
  • 接下来,我们看到 NamesTestCase 中的 test_first_last_name() 导致了错误。

测试未通过时怎么办

  • 如果你检查的条件没错,测试通过意味着函数的行为是对的,而测试未通过意味着编写的新代码有错。
  • 因此,测试未通过时,不要修改测试,而应修复导致测试不能通过的代码:检查刚刚对函数所做的修改,找出导致函数行为不符合预期的修改。
  • 就这里而言,最佳的选择是让中间名变为可选的。
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()
  • 现在,再次运行测试,将得到如下结果:
.
----------------------------------------------------------------------
Ran 1 test in 0.000s

OK

添加新测试

  • 确定 get_formatted_name() 又能正确处理简单的姓名后,我们再编写一个测试,用于测试包含中间名的姓名。为此,在 NamesTestCase 类中再添加一个方法:
import unittest
from name_fucntion import get_formatted_name

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

    def test_first_last_name(self):
        """能够正确处理像 Janis Joplin 这样的姓名吗?"""
        formatted_name = get_formatted_name('janis', 'joplin')
        self.assertEqual(formatted_name, 'Janis Joplin')

    def test_first_middle_last_name(self):
        """能够正确处理像 Wolfgang Amadeus Mozart这样的姓名吗?"""
        fortmatted_name = get_formatted_name('wolfgang', 'mozart', 'amadeus')
        self.assertEqual(fortmatted_name, 'Wolfgang Amadeus Mozart')

if __name__ == '__main__':
    unittest.main()
  • 将该方法命名为 test_first_last_middle_name()。方法名必须以 test_ 打头,这样它才会在我们运行 test_name_function.py 时自动运行。
  • 再次运行 test_name_function.py 时,两个测试都通过了:
..
----------------------------------------------------------------------
Ran 2 tests in 0.000s

OK

测试类

  • 下面来编写针对类的测试。
  • 很多程序中都会用到类,因此证明你的类能够正确工作大有裨益。

各种断言方法

  • Python 在 unittest.TestCase 类中提供了很多断言方法。
  • 断言方法检查你认为应该满足的条件是否确实满足。如果该条件确实满足,你对程序行为的假设就得到了确认,可以确信其中没有错误。如果你认为应该满足的条件实际上并不满足,Python 将引发异常。
方法用途
assertEqual(a, b)核实 a == b
assertNotEqual(a, b)核实 a != b
assertTrue(x)核实 x 为 True
assertFalse(a, b)核实 x 为 False
assertIn(item, list)核实 itemlist
assertNotIn(item, list)核实 item 不在 list

一个要测试的类

  • 类的测试与函数的测试相似,你所做的大部分工作是测试类中方法的行为。
  • 先定义一个类:
class AnonymousSurvey:
    """手机匿名调查问卷的答案。"""

    def __init__(self, question):
        """存储一个问题,并为存储答案做准备。"""
        self.question = question
        self.reponses = []
    
    def show_question(self):
        """显示调查问卷。"""
        print(self.question)
    
    def store_response(self, new_response):
        """存储单份调查问卷。"""
        self.reponses.append(new_response)

    def show_result(self):
        """显示收到的所有答卷。"""
        print("Survey results:")
        for response in self.reponses:
            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.")

while True:
    response = input("Language: ")
    
    if response == 'q':
        break
    my_survey.store_response(response)

# 显示调查结果。
print("Thank you to everyone who participated in the survey!")
my_survey.show_result()

测试 AnonymousSurvey

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('English')
        self.assertIn('English', my_survey.responses)

if __name__ == "__main__":
    unittest.main()
  • 测试通过:
.
----------------------------------------------------------------------
Ran 1 test in 0.000s

OK
  • 下面来核实当用户提供三个答案时,它们也将被妥善地存储。
import unittest

from survey import AnonymousSurvey

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

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

if __name__ == "__main__":
    unittest.main()
  • 测试通过:
..
----------------------------------------------------------------------
Ran 2 tests in 0.000s

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

方法 setUp()

  • 在前面的测试中,我们在每个测试方法中都创建了一个 AnonymousSurvey 实例,并在每个方法中都创建了答案。
  • unittest.TestCase 类包含的方法 setUp() 让我们只需创建这些对象一次,就能在每个测试方法中使用。
  • 如果在 TestCase 类中包含了方法 setUp(),Python 将先运行它,再运行各个以 test_ 打头的方法。
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 = ['English', 'Spanish', 'Mandarin']
        # return super().setUp()

    def test_store_single_responses(self):
        """测试单个答案会被妥善存储。"""
        self.my_survey.store_response('English')
        self.assertIn('English', 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()
  • 测试通过:
..
----------------------------------------------------------------------
Ran 2 tests in 0.000s

OK
  • 方法 setUp() 做了两件事情:创建一个调查对象,以及创建一个答案列表。
  • 存储这两样东西的变量名包含前缀 self(即存储在属性中),因此可在这个类的任何地方使用。

运行测试用例时,每完成一个单元测试,Python 都打印一个字符:测试通过时打印一个句点 .,测试引发错误时打印一个 E,而测试导致断言失败时则打印一个 F