为了保证软件的可靠性和安全性,我们可以为函数或类编写测试代码,这样不仅仅可以简化我们手动测试的工作,也帮助我们未来对软件的维护和再开发更加方便。

比如我们创建一个函数,将用户的姓和名作为输入,然后返回一个格式化之后的全名:

# name_function.py
def get_formatted_name(first, last):
    full_name = f"{first} {last}"
    return full_name.title()

若要测试函数的正确性,我们在names.py中创建一个程序不断地提示用户输入名字,并打印出格式化之后的全名,然后我们可以自己来判断输出的正确性:

# names.py
from name_function import get_formatted_name
print("Enter 'q' at any time to quit.")
while True:
    first = input("\nfirst name: ")
    if first == 'q':
        break
    last = input("\nlast name: ")
        break
    formatted_name = get_formatted_name(first, last)
    print(f"\tNeatly formatted name: {formatted_name}.")

运行代码后,我们发现格式化之后的名字是正确的,但是如果希望格式化名字是全部大写,修改完函数之后,我们需要继续运行names.py来进行判断,这种手动检查非常麻烦,而且我们自己难免也有判断失误的时候。在Python中,我们可以使用自带的模块unittest进行单元测试,单元测试可以对一个模块、一个函数或者一个类进行正确性检验的测试工作,提升我们的测试效率。

测试函数

若要创建一个测试用例,我们需要创建一个类,继承unittest.TestCase,然后在类的内部根据程序的需求,加入测试的具体方法:

# name_function_testing.py
import unittest
from name_function import get_formatted_name

class NamesTestCase(unittest.TestCase):
    def test_first_last_name(self):
        formatted_name = get_formatted_name('andrew', 'yang')
        self.assertEqual(formatted_name, 'Andrew Yang')

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

在上面的代码中,我们使用简单的self.assertEqual就能检验代码的正确性,运行代码后,我们会发现终端有以下的信息出现,来告诉我们测试的通过率:

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

OK

第一行的句号告诉我们一个测试通过了,下一行告诉我们所有测试花费的时间,最后一行的OK告诉我们这个测试用例通过了。有了这些信息,我们的测试结果更加直观。

如果我们把格式化名字的函数进行以下的修改,需要用户输入自己的middle name:

# name_function.py
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)
-----------------------------------------------------------
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)

上面的信息提示测试没有通过,第一行的E代表测试中有一个错误(Error),下一行告诉我们具体失败的测试函数 test_first_last_name,之后的Trackback告诉我们具体的错误信息,在这个例子中就是漏了一个参数last。最后两行则是测试数量统计。当我们运行很多的测试用例时,这些具体信息对我们debug非常有帮助。

为了修复上面测试出现的问题,我们可以对格式化函数做以下的修改:

# name_function.py
def get_formatted_name(first, last, middle=''):
    """Generate a neatly formatted full name.""" 
    if middle:
        full_name = f"{first} {middle} {last}" 
    else:
        full_name = f"{first} {last}" return full_name.title()

我们再给测试用例添加额外的测试:

# test_name_function.py
class NamesTestCase(unittest.TestCase):

def test_first_last_name(self):
    formatted_name = get_formatted_name('andrew', 'yang')
    self.assertEqual(formatted_name, 'Andrew Yang')

def test_first_last_middle_name(self):
    get_formatted_name( 'wolfgang', 'mozart', 'amadeus') 
    self.assertEqual(formatted_name, 'Wolfgang Amadeus Mozart')

if __name__ == '__main__':

unittest.main()

当我们再次运行测试代码的时候,我们则会得到以下信息:

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

OK

测试一个类

除了为单个函数写测试外,我们也可以为类写测试,来帮助类的内部函数运行正确。以下是常用的assert方法,帮助我们进行各种不同的测试:

方法名使用
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中

测试类和测试函数很相似,大部分测试工作也是测试类内部的方法。我们先来创建一个能统计问卷调查的类:

class AnonymousSurvey:
    """Collect anonymous answers to a survey question."""

    def __init__(self, question):
        """Store a question, and prepare to store responses."""
        self.question = question
        self.responses = []

    def show_question(self):
        """Show the survey questions."""
        print(self.question)
    
    def store_response(self, new_response):
        """Store a single response to the survey."""
        self.responses.append(new_response)
    
    def show_results(self):
        """Show all the responses that have been given."""
        print("Survey results:")
        for response in self.responses:
            print(f"- {response}")

这个类可以根据调查的问题,收集答案,并且可以把全部回答打印出来。如果要测试答案,我们可以写以下的测试:

# test_survey.py
import unittest
from survey import AnonymousSurvey

class TestAnonymousSurvey(unittest.TestCase):
    """Tests for the class AnonymousSurvey"""

    def test_store_single_response(self):
        """Test that a single response is stored properly."""
        question = "What language did you first learn to speak?"
        my_survey = AnonymousSurvey(question)
        my_survey.store_response('Fuzhouness')
        self.assertIn('Fuzhouness', my_survey.responses)

    def test_store_three_responses(self):
        question = "What language did you first learn to speak?"
        my_survey = AnonymousSurvey(question)
        responses = ['Fuzhouness', "Chinese", "English"]
        for response in responses:
            my_survey.store_response(response)
        self.assertIn('English', my_survey.responses)
    
if __name__ == '__main__':
    unittest.main()

运行测试案例后,我们应该可以看到以下两个测试通过的信息:

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

OK

在上面的代码中,我们在每一个测试函数内创建了一个AnonymousSurvey实例,为了简化测试,我们可以在测试类中创建一个setUp()函数,并在其中创建一个公用的AnonymousSurvey实例。在Python中,当我们在一个TestCase类中创建setUp函数后,再次运行测试函数时,Python会在运行内部方法前优先运行setUp函数:

import unittest
from survey import AnonymousSurvey

class TestAnonymousSurvey(unittest.TestCase):

    def setUp(self):
        question = "What language did you first learn to speak?"
        self.my_survey = AnonymousSurvey(question)
        self.responses = ['Fuzhounese', 'Chinese', 'English']

    def test_store_single_response(self):
        question = "What language did you first learn to speak?"
        slef.my_survey.store_response(self.response(self.response[0])
        self.assertIn('Fuzhouness', my_survey.responses)

    def test_store_responses(self):
        question = "What language did you first learn to speak?"
        for response in responses:
            self.my_survey.store_response(response)
        for response in responses:
            self.assertIn(response, self.my_survey.responses)
    
if __name__ == '__main__':
    unittest.main()

运行代码后,则会返回测试结果。可以看到,在我们使用单用测试后,代码的测试和维护过程更加的简便,也让我们的测试流程更加迅速。

实践练习

请大家在先创建一个要被测试的字典类:

# mydict.py
class MyDict(dict):
    def __init__(self, **kw):
        super().__init__(**kw)

    def put(self, key, value):
        self[key] = value

    def get(self, key):
        return self[key]

然后请大家编写自己的测试用例,来测试字典的set和get函数,以下是答案:

import unittest 
from mydict import MyDict

class TestDict(unittest.TestCase):
    def setUp(self): 
        self.keys = {'a', 'b', 'c', 'd'}
        self.values = {'1', '2', '3', '4'}
        self.dict = MyDict()
    
    def testDict(self):
        for i in range(0, len(self.keys)):
            self.dict.put(self.keys.pop(), self.values.pop())
        for i in range(0, len(self.keys)):
            self.assertEqual(self.dict.get[self.keys[i]], self.values[i])

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

进阶学习建议

想要彻底掌握Python,我推荐这4本书: 《Python Crash Course》, 《Python Cookbook》, 《Fluent Python》, 《500 Lines or Less》。

刚刚入门编程的小伙伴可以先从Python Crash Course开始,如果已经看完了我的视频,直接从这本书中的项目练习开始,通过实践加强对编程语言的理解。如果不是Python新手,有相关Python的编程经验,我推荐Python Cookbook和Fluent Python这两本书,来进阶Python的使用。

掌握Python的语法之后,可以通过在实践项目应用Python,达到知行合一的境界。最后推荐大家去看”500 Lines or Less“这个项目,其中包含了使用Python编写的各种项目,很多都是领域大牛写的,包括但不限于电子表格、网络爬虫、代码提示、数据库、3D设计等等。其中不仅仅有源码,也有文字介绍

希望大家未来能学得开心,将Python化为手中的神器 :)