Loading

11 第十一章 测试代码

测试函数

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

# name_function.py
def get_formatted_name(first, last):
    """Generate a neatly formatted full name."""
    full_name = 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("\nPlease 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("\tNeatly formatted name: " + formatted_name + '.')
# test_name_function.py
import unittest
from name_function 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')

unittest.main()

使用了unittest 类最有用的功能之一:一个断言 方法;
断言方法用来核实得到的结果是否与期望的结果一致

.
----------------------------------------------------------------------
Ran 1 test in 0.000s

OK

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

# name_function.py
# 运行test_name_function.py
def get_formatted_name(first, last):
    """Generate a neatly formatted full name."""
    full_name = first + ' ' + middle + ' ' + last
    return full_name.title()
E
======================================================================
ERROR: test_first_last_name (__main__.NamesTestCase)
----------------------------------------------------------------------
Traceback (most recent call last):
File "test_name_function.py", line 8, 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.000s
FAILED (errors=1)

第1行输出只有一个字母E,它指出测试用例中有一个单元测试导致了错误;
接下来,我们看到NamesTestCase 中的test_first_last_name() 导致了错误;
测试用例包含众多单元测试时,知道哪个测试未通过至关重要;
我们看到了一个标准的traceback,它指出函数调用get_formatted_name('janis', 'joplin') 有问题,因为它缺少一个必不可少的位置实参;
我们还看到运行了一个单元测试;
最后,还看到了一条消息,它指出整个测试用例都未通过,因为运行该测试用例时发生了一个错误;
这条消息位于输出末尾,让你一眼就能看到——你可不希望为获悉有多少测试未通过而翻阅长长的输出

# name_function.py
def get_formatted_name(first, last, middle=''):
    """生成整洁的姓名"""
    if middle:
        full_name = first + ' ' + middle + ' ' + last
    else:
        full_name = first + ' ' + last
    return full_name.title()
# 运行test_name_function.py
.   
----------------------------------------------------------------------
Ran 1 test in 0.000s  

OK
import unittest
from name_function 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_last_middle_name(self):
        """能够正确地处理像Wolfgang Amadeus Mozart这样的姓名吗?"""
        formatted_name = get_formatted_name(
        'wolfgang', 'mozart', 'amadeus')
        self.assertEqual(formatted_name, 'Wolfgang Amadeus Mozart')

unittest.main()

在TestCase 类中使用很长的方法名是可以的;
这些方法的名称必须是描述性的,这才能让你明白测试未通过时的输出

..  
----------------------------------------------------------------------  
Ran 2 tests in 0.000s  

OK

测试类

img

# survey.py
class AnonymousSurvey():
    """收集匿名调查问卷的答案"""

    def __init__(self, question):
        """存储一个问题,并为存储答案做准备"""
        self.question = question
        self.responses = []

    def show_question(self):
        """显示调查问卷"""
        print(question)

    def store_response(self, new_response):
        """存储单份调查答卷"""
        self.responses.append(new_response)

    def show_results(self):
        """显示收集到的所有答卷"""
        print("Survey results:")
        for response in responses:
            print('- ' + response)
# language_survey.py
from survey import AnonymousSurvey

#定义一个问题,并创建一个表示调查的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()
# test_survey.py
import unittest
from survey import AnonymousSurvey

class TestAnonmyousSurvey(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)

unittest.main()
.   
----------------------------------------------------------------------
Ran 1 test in 0.001s  

OK
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)

    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)

unittest.main()
..  
----------------------------------------------------------------------
Ran 2 tests in 0.000s  

OK

unittest.TestCase 类包含方法setUp() ,让我们只需创建对象一次,并在每个测试方法中使用它们;
如果你在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 = ['English', 'Spanish', 'Mandarin']

    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)

unittest.main()

可在setUp() 方法中创建一系列实例并设置它们的属性,再在测试方法中直接使用这些实例;
相比于在每个测试方法中都创建实例并设置其属性,这要容易得多

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

小结

如何使用模块unittest 中的工具来为函数和类编写测试;
如何编写继承unittest.TestCase 的类,以及如何编写测试方法,以核实函数和类的行为符合预期;
如何使用方法setUp() 来根据类高效地创建实例并设置其属性,以便在类的所有测试方法中都可使用它们;
参与工作量较大的项目时,你应对自己编写的函数和类的重要行为进行测试;
这样你就能够更加确定自己所做的工作不会破坏项目的其他部分,你就能够随心所欲地改进既有代码了;
如果不小心破坏了原来的功能,你马上就会知道,从而能够轻松地修复问题;
相比于等到不满意的用户报告bug后再采取措施,在测试未通过时采取措施要容易得多;
如果你在项目中包含了初步测试,其他程序员将更敬佩你,他们将能够更得心应手地尝试使用你编写的代码,也更愿意与你合作开发项目;
如果你要跟其他程序员开发的项目共享代码,就必须证明你编写的代码通过了既有测试,通常还需要为你添加的新行为编写测试;
请通过多开展测试来熟悉代码测试过程;
对于自己编写的函数和类,请编写针对其重要行为的测试,但在项目早期,不要试图去编写全覆盖的测试用例,除非有充分的理由这样做

posted @ 2023-04-21 20:57  Artwalker  阅读(7)  评论(0编辑  收藏  举报
Live2D