Python个人学习笔记(10)——测试代码

测试代码

编写函数和类时,还可以为其编写测试.通过测试,可确定代码面对各种输出都能够按要求的那样工作.在程序中添加新的代码时,你也可以对其进行测试,确定它们不会破坏程序既有的行为.程序员都会犯错,因此每个程序员都必须经常测试其代码,在用户发现问题前找出它们.
学习目标:
学习如何使用Python模块unittest中的工具来测试代码.
学习编写测试用例,核实一系列输入都将得到预期的输出

测试函数

要测试函数,得有要测试的代码.然后,大家也都会运行程序,然后进行相应的操作,再根据程序做出的应答判断程序是否正常.但不可置否,这个步骤太繁琐了.所幸python提供了一种自动测试函数输出的高效方式.

  • 单元测试和测试用例
    python标准库中的模块unittest提供了代码测试工具.单元测试用于核实函数的某个方面没有问题;测试用例是一组单元测试,这些单元测试一起核实函数在各种行为都符合要求.良好的测试用例考虑到函数可能收到的各种输入,包含针对所有这些情形的测试.全覆盖式测试用例包含一整套单元测试,涵盖了各种可能的函数使用方式.对于大型项目,要实现全覆盖可能很难.通常,最初只需要针对代码的重要行为编写测试即可,等项目被广泛使用时在考虑全覆盖.
  • 可通过的测试
    要为函数写测试用例,可先导入模块unittest以及要测试的函数,再创建一个继承unittest.TestCase的类,并编写一系列方法去对函数行为的不同方向进行测试.下面是一个简单的函数:
    name_function.py
def get_formatted_name(first,last):
    """生成整洁的姓名"""
    full_name = first + ' ' + last
    return full_name.title()

下面是仅包含一个方法的测试用例,用于检查get_formtted_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()

运行结果:

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

OK
  • 首先,我们导入了模块unittest和要测试的函数get_formatted_name().
  • 创建一个NamesTestCase的类,用于包含一系列针对get_formatted_name()的单元测试.可以随便对这个类命名,但最好让它看起来要和要测试的函数相关,并包含Test字样.这个类必须继承unittest.TestCase类——这样python才知道如何运行你编写的测试。
  • 在上面,NamesTestCase类只包含一个方法,用于测试get_formatted_name()的一个方面.
  • 在下面这条代码,我们使用了unittest类最有用的功能之一:一个断言方法.断言方法是用来核实得到的结果是否与期望的结果一致.这条代码行的意思是说:将formatted_name值与字符串’Janis Joplin’作比较,如果它们相等,则万事大吉,如果不相等,跟我说一声.事实上,运行结果告诉我们,代码的功能跟预期一致.
self.assertEqual(formatted_name,'Janis Joplin')
  • 不能通过的测试
    我们修改get_formatted_name()方法:
    name_function.py
def get_formatted_name(first,middle,last):
    """生成整洁的姓名"""
    full_name = first + ' ' + middle + ' ' + last
    return full_name.title()

再运行test_name_function.py程序,有:

E
======================================================================
ERROR: test_first_last_name (__main__.NamesTestCase)
能够正确地处理像Janis Joplin这样的姓名吗?
----------------------------------------------------------------------
Traceback (most recent call last):
  File "d:\vscode\test.py", line 10, 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.002s

FAILED (errors=1)

让我们来分析一下:

  • 第一行输出只有一个E,它指出测试用例中有一个单元测试导致了错误.
  • 接下来,我们看到NamesTestCase中的test_first_last_name导致了错误
  • 我们还看到一个标准的traceback,它指出函数调用get_formatted_name(‘janis’,‘joplin’)的问题.因为它缺少了一个必不可少的位置实参
  • 我们还看到了运行了一个单元测试
  • 最后还看到了一条信息,它指出整个单元测试没能通过,因为运行该测试用例时发生了一个错误.这条错误位于输出末端,让你一眼就能看到,你不必为获悉有多少测试没通过而翻阅长长的输出.
  • 测试没通过怎么办?
    我们对方法get_formatted_name()进行一些修改:
    • 让中间名变为可选的
    • 在函数定义时,将形参middle移到形参末尾,并将其默认值设为一个空字符串
    • 还要添加一个if测试,以便根据是否提供了中间名相应地创建姓名
      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()

以及在类NamesTestCase中增加一个新的测试单元

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_middle_last_name(self):
        """能正常处理像Wolfgang Amadeus Mozart这样的姓名吗?"""
        formatted_name = get_formatted_name('Wolfgang','Mozart','Amadeus')
        self.assertEqual(formatted_name,'Wolfgang Amadeus Mozart')

unittest.main()

运行结果:

..
----------------------------------------------------------------------
Ran 2 tests in 0.001s

OK

现在,两个测试用例都通过了.这让我们深信这个函数既能正确地处理像Janis Joplin这样的姓名,也能正确到处理像Wolfgang Amadeus Mozart这样的姓名.

测试类

在前面,我们编写了针对单个函数的的测试,下面来编写针对类的测试

  • 各种断言方法
    python在unittest.TestCase类中提供了很多断言方法.前面说过,断言方法检查你认为应该满足的条件是否得到满足.如果该条件确实满足,你对程序行为的假设就得到了确认,你就可以确信其中没有错误.如果你认为应该满足的条件没有得到满足,Python将引发异常.

下面列表中描述了6个常用的断言方法:

方法 用途
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中
  • 测试AnonymyousSurvey类
    类的测试与函数的测试相似——你所做的大部分工作都是测试类中方法的行为,但存在一些不同之处.
    survey.py
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 results:")
        for response in self.responses:
            print('- ' + response)

接下来编写一个测试,对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)

    def test_store_single_response(self):
        """测试多个个答案能否被正常存储"""
        question = "What language did you first learn to speak?"
        my_survey = AnonymousSurvey(question)
        responses = ['English','Chinese','English']
        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.001s

OK
  • 首先,我们依旧是要导入unittest模块以及AnonymousSurvey类
  • 分别写两个测试方法对不同情形进行测试
  • 最后,两个测试都通过了

前面的做法效果很好,但这些代码有重复的地方,下面将解决这一问题

  • 方法setUp()
    在前面的测试中,我们在每个测试方法都创建了一个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 = ['English','Chinese','English']

    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_response(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()

运行结果:

..
----------------------------------------------------------------------
Ran 2 tests in 0.002s

OK
  • 方法setUp()做了两件事:创建一个调查对象;创建一个答案列表.存储这两样东西的变量包含前缀self(即包含在属性内),因此可以在类的任何地方使用.这让两个测试变得更简单,因为它们都不用创建新的对象和答案.
  • 测试自己编写的类时,方法setUp()让测试方法编写起来更简单:可在setUp()方法中创建一系列示例并设置它们的属性,再在测试方法中直接使用这些实例.

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

个人从书本学到的知识,作为自己学习笔记的同时,与各位朋友分享,如有不足,请多多指教

参考文献:《Python编程从入门到实践》【美】Eric Matthes 著 袁国忠 译

你可能感兴趣的:(Python个人学习笔记,单元测试,python,编程语言)