在Python编程过程中用单元测试法调试代码的介绍_python

对于程序开发新手来说,一个最常见的困惑是测试的主题。他们隐约觉得“单元测试”是很好的,而且他们也应该做单元测试。但他们却不懂这个词的真正含义。如果这听起来像是在说你,不要怕!在这篇文章中,我将介绍什么是单元测试,为什么它有用,以及如何对Python的代码进行单元测试。

什么是测试?

在讨论为什么测试很有用、怎样进行测试之前,让我们先花几分钟来定义一下“单元测试”究竟是什么。在一般的编程术语中,“测试”指的是通过编写可以调用的代码(独立于你实际应用程序的代码)来帮助你确定程序中是否有错误。这并不能证明你的代码是正确的(在非常有限的情况下这是唯一的可能)。它只是报告了测试者认为的那种情况是否被正确处理了。

注:当我使用“测试”一次时,我指的是“自动化测试”,即这些测试是在机器上运行的。“手动测试”则是一个人运行程序,并与它进行交互,从而发现漏洞,这是个独立的概念。

测试可以检查出什么样的情况呢?语法错误是语言的意外误用,如
 

my_list..append(foo)

后面多余的一个 “.“。逻辑错误是当算法(可以看成是“解决问题的方式”)不正确时引发的。可能程序员忘记Python是“零索引“的并且试图通过写
 

print(my_string[len(my_string)])

(这样会引起IndexError)来打印出一个字符串中的最后一个字符。更大、更系统的错误也可以被检查出来。比如当用户输入一个大于100的数字、或者在网站检索不可用的时候挂起此网站的话,程序会一直崩溃。

这些所有的错误都可以通过对代码的仔细测试检查出来。Unit testing,特指在一个分隔的代码单元中的测试。一个单元可以是整个模块,一个单独的类或者函数,或者这两者间的任何代码。然而,重要的是,测试代码要与我们没有测试到的其他代码相互隔离(因为其它代码本身有错误的话会因此混淆测试结果)。考虑如下例子:
 

def is_prime(number):
  """Return True if *number* is prime."""
  for element in range(number):
    if number % element == 0:
      return False

  return True

def print_next_prime(number):
  """Print the closest prime number larger than *number*."""
  index = number
  while True:
    index += 1
    if is_prime(index):
      print(index)

你有两个函数,is_prime和print_next_prime。如果你想测试print_next_prime,我们就需要确定is_prime是正确的,因为print_next_prime中调用了这个函数。在这种情况下,print_next_prime函数是一个单元,is_prime函数是另一个单元。由于单元测试每次只测试一个单元,因此我们需要仔细考虑怎样才能准确的测试print_next_prime?(更多的是关于之后怎样实现这些测试)。

因此,测试代码应该长什么样呢?如果上一个例子存在一个叫primes.py的文件中,我们可以把测试代码写在一个叫test_primes.py的文件中。下面是test_primes.py 中的最基本内容,比如下面这个测试样例:
 

import unittest
from primes import is_prime

class PrimesTestCase(unittest.TestCase):
  """Tests for `primes.py`."""

  def test_is_five_prime(self):
    """Is five successfully determined to be prime?"""
    self.assertTrue(is_prime(5))

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

这个文件通过一个test case :? test_is_five_prime. 创建了一个单元测试。通过Python内嵌的一个测试框架unittest。当unittest.main()被调用时,任何一个以test开头命名的成员函数将被运行,他们是unittest.TestCase的一个派生类,并且是断言检查的。如果我们通过输入python test_primes.py来运行测试,我们能够看到unittest框架在控制台上 的输出:
 

$ python test_primes.py
E
======================================================================
ERROR: test_is_five_prime (__main__.PrimesTestCase)
----------------------------------------------------------------------
Traceback (most recent call last):
File "test_primes.py", line 8, in test_is_five_prime
  self.assertTrue(is_prime(5))
File "/home/jknupp/code/github_code/blug_private/primes.py", line 4, in is_prime
  if number % element == 0:
ZeroDivisionError: integer division or modulo by zero

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

单独的“E”表示的是我们单元测试的结果(如果它成功了,会打印出一个“.”)。我们可以看到我们的测试失败了,以及导致失败的那行代码,还有任何引发的异常信息。

为什么要测试?

在我们继续那个例子之前,要问个很重要的问题:“为什么测试对我来说有价值”?这是个公平的问题,也是那些对于代码测试不熟悉的人常问的问题。毕竟,测试需要一定的时间,而我们完全可以用这些时间去编代码,为什么要测试而不是去做那些最有生产效率的事?

有很多答案可以有效的回答这个问题,我列出了以下几点:

    测试可以保证你的代码在一系列给定条件下正常工作

测试确保了一系列条件下的正确性。语法错误基本上一定通过测试被查出来,一个代码单元的基本的逻辑也可以通过测试被检测出来,以确保一定条件下的正确性。再次,它不是要证明代码是在任何条件下都正确的。我们只是简单的瞄准了一套比较完整的可能的条件(例如,你可以写一个测试来监测当你调用my_addition_function(3, 'refrigerator), 的时候,但你不必为每个参数检测所有可能的字符串)

    测试允许人们确保对代码的改动不会破坏现有的功能

重构代码时,这一点特别有用。如果没有测试到位,你就没法保证你的代码的改变没有破坏之前工作正常的东西。如果你希望更改或重写你的代码,并希望不会破坏任何东西,适当的单元测试是很必要的。

    测试迫使人们在不寻常条件的情况下思考代码,这可能会揭示出逻辑错误

编写测试强迫你去思考在非正常条件下你的代码可能遇到的问题。在上面的例子中,my_addition_function函数可以将两个数字相加。测试基本正确性的简单测试将调用my_addition_function(2,2),并断言说结果是4。然而,进一步的测试可能会通过调用my_addition_function(2.0,2.0)来测试该功能是否能正确进行浮点数的运算。防御性的编码原则表明你的代码应该能够在非法输入的情况下正常失效,因此测试时,当字符串类型被作为参数传递到函数中时应当抛出一个异常。

    良好的测试要求模块化,解耦代码,这是一个良好的系统设计的标志

单元测试的整体做法是通过代码的松散耦合使其变得更容易。如果你的应用程序代码直接调用数据库,例如,测试你应用程序的逻辑依赖于一个有效的数据库连接,并且测试数据要存在于数据库中。另一方面,隔离了外部资源的代码在测试过程中更容易被模拟对象所替代。出于必要,(人们)设计的有测试能力的应用程序最终采用了模块化和松散耦合。

单元测试的剖析

通过继续之前的例子,我们将看到如何编写并组织单元测试。回想一下,primes.py包含以下代码:

def is_prime(number):
  """Return True if *number* is prime."""
  for element in range(number):
    if number % element == 0:
      return False

  return True

def print_next_prime(number):
  """Print the closest prime number larger than *number*."""
  index = number
  while True:
    index += 1
    if is_prime(index):
      print(index)

同时,文件test_primes.py包含如下代码:
 

import unittest
from primes import is_prime

class PrimesTestCase(unittest.TestCase):
  """Tests for `primes.py`."""

  def test_is_five_prime(self):
    """Is five successfully determined to be prime?"""
    self.assertTrue(is_prime(5))

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

做出断言

unittest是Python标准库中的一部分,并且也是我们开始“单元测试之旅”的一个好的起点。一个单元测试中包括一个或多个断言(一些声明被测试代码的一些属性为真的语句)。会想你上学的时候“断言”这个词的字面意思就是“陈述事实”。在单元测试中,断言也是同样的作用。

self.assertTrue 更像是自我解释。它能声明传递过去的参数的计算结果为真。unittest.TestCase类包含了许多断言方法,所以一定要检查列表并选择合适的方法进行测试。如果在每个测试中都用到assertTrue的话,则应该考虑一个反模式,因为它增加了测试中读者的认知负担。正确使用断言的方法应当是使测试能够明确说明究竟是什么在被断言(例如,很明显?,只需扫一眼assertIsInstance 的方法名,就知道它要说明的是其参数)。

每个测试应该测试一个单独、有具体特性的代码,并且应该被赋予相关的命名。就单元测试发现机制的研究表明(主要在Python2.7+和3.2+版本中),测试方法应该以test_为前缀命名。(这是可配置的,但是其目的是鉴别测试方法和非测试的实用方法)。如果我们把test_is_five_prime 的命名改为is_five_prime的话,运行python中的test_primes.py时会输出如下信息:
 

$ python test_primes.py

----------------------------------------------------------------------
Ran 0 tests in 0.000s

OK

不要被上面信息中的“OK”所糊弄了,只有当什么测试都没真正运行的时候才会显示出“OK”!我认为一个测试也没跑其实应该显示个报错的,但是个人感觉放在一边,这是一个你应该注意是行为,尤其是当通过程序运行来检查测试结果的时候(例如,一个持续的集成工具,像TracisCI)。

异常

让我们回到test_primes.py的实际内容中去,回忆一下运行python test_primes.py指令后的输出结果:
 

$ python test_primes.py
E
======================================================================
ERROR: test_is_five_prime (__main__.PrimesTestCase)
----------------------------------------------------------------------
Traceback (most recent call last):
File "test_primes.py", line 8, in test_is_five_prime
  self.assertTrue(is_prime(5))
File "/home/jknupp/code/github_code/blug_private/primes.py", line 4, in is_prime
  if number % element == 0:
ZeroDivisionError: integer division or modulo by zero

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

这些输出告诉我们,我们一个测试的结果失败并不是因为一个断言失败了,而是因为出现了一个未捕获的异常。事实上,由于抛出了一个异常,unittest框架并没有能够运行我们的测试就返回了。

这里的问题很明确:我们使用的求模运算的计算范围中包括了0,因此执行了一个除以0的操作。为了解决这个问题,我们可以很简单的将起始值由0变为2,并指出对0求模是错误的,而对1求模则一直是真(并且一个素数只能被自身和1整除,因此我们无需检查1)。

解决问题

一次失败的测试使我们修改了代码。一旦我们改好了这个错误(将s_prime中的一行改为for element in range(2, number):),我们就得到了如下输出:
 

$ python test_primes.py
.
----------------------------------------------------------------------
Ran 1 test in 0.000s

现在错误已经改了,这是不是意味着我们应该删掉test_is_five_prime这个测试方法(因为很明显,它将不会一直能通过测试)?不应该删。由于通过测试是最终目标的话单元测试应该尽量少的被删除。我们已经测试过is_prime的语法是有效的,并且,至少在一种情况下,它返回正确的结果。我们的目标是要建立一套能全部通过的(单元测试的逻辑分组)测试,虽然有些一开始可能会失败。

test_is_five_prime用于处理一个“非特殊”的素数。让我们确保它也能正确处理非素数。将以下方法添加到PrimesTestCase类:
 

def test_is_four_non_prime(self):
  """Is four correctly determined not to be prime?"""
  self.assertFalse(is_prime(4), msg='Four is not prime!')

请注意,这时我们给assert调用添加了可选的msg参数。如果该测试失败了,我们的信息将被打印到控制台,并给运行测试的人提供额外的信息。

边界情况

我们已经成功的测试了两种普通情况。现在让我们考虑边界情况下、或者那些不寻常或意外的输入的用例。当测试一个其范围是正整数的函数时,边界情况下的实例包括0、1、负数和一个很大的数字。现在让我们来测试其中的一些。

添加一个对0的测试很简单。我们预计?is_prime(0)返回的是false,因为,根据定义,素数必须大于1。
 

def test_is_zero_not_prime(self):
  """Is zero correctly determined not to be prime?"""
  self.assertFalse(is_prime(0))

可惜呀,输出是:
 

python test_primes.py
..F
======================================================================
FAIL: test_is_zero_not_prime (__main__.PrimesTestCase)
Is zero correctly determined not to be prime?
----------------------------------------------------------------------
Traceback (most recent call last):
File "test_primes.py", line 17, in test_is_zero_not_prime
  self.assertFalse(is_prime(0))
AssertionError: True is not false

----------------------------------------------------------------------
Ran 3 tests in 0.000s

FAILED (failures=1)

0被错误的判定为素数。我们忘记了,我们决定在数字范围中跳过0和1。让我们增加一个对他们的特殊检查。
 

def is_prime(number):
  """Return True if *number* is prime."""
  if number in (0, 1):
    return False

  for element in range(2, number):
    if number % element == 0:
      return False

  return True

现在测试通过了。我们的函数应该怎样处理一个负数?在写这个测试用例之前就知道输出结果是很重要的。在这种情况下,任何负数都应该返回false。
 

def test_negative_number(self):
  """Is a negative number correctly determined not to be prime?"""
  for index in range(-1, -10, -1):
    self.assertFalse(is_prime(index))

这里我们觉得检查从-1到-9的所有数字。在一个循环中调用test方法是非常合法的,在一个测试中多次调用断言方法也可以。我们可以在下面用(更详细)的方式改写代码。
 

def test_negative_number(self):
  """Is a negative number correctly determined not to be prime?"""
  self.assertFalse(is_prime(-1))
  self.assertFalse(is_prime(-2))
  self.assertFalse(is_prime(-3))
  self.assertFalse(is_prime(-4))
  self.assertFalse(is_prime(-5))
  self.assertFalse(is_prime(-6))
  self.assertFalse(is_prime(-7))
  self.assertFalse(is_prime(-8))
  self.assertFalse(is_prime(-9))

这两个是完全等价的。除了当我们运行循环版本时,我们得到了一个我们不太想要的信息:

python test_primes.py
...F
======================================================================
FAIL: test_negative_number (__main__.PrimesTestCase)
Is a negative number correctly determined not to be prime?
----------------------------------------------------------------------
Traceback (most recent call last):
File "test_primes.py", line 22, in test_negative_number
  self.assertFalse(is_prime(index))
AssertionError: True is not false

----------------------------------------------------------------------
Ran 4 tests in 0.000s

FAILED (failures=1)

嗯···我们知道测试失败了,但是是在哪个负数上失败的?非常没用的是,Python的单元测试框架并没有打印出预期值和实际值。我们可以移步到两种方式上,并用其中之一来解决问题:通过msg参数,或通过使用一个第三方的单元测试框架。

使用msg参数来assertFalse仅仅能够使我们认识到我们可以用字符串的格式设置来解决问题。
 

def test_negative_number(self):
  """Is a negative number correctly determined not to be prime?"""
  for index in range(-1, -10, -1):
    self.assertFalse(is_prime(index), msg='{} should not be determined to be prime'.format(index))

从而给出了如下输出信息:
 

python test_primes
...F
======================================================================
FAIL: test_negative_number (test_primes.PrimesTestCase)
Is a negative number correctly determined not to be prime?
----------------------------------------------------------------------
Traceback (most recent call last):
File "./test_primes.py", line 22, in test_negative_number
  self.assertFalse(is_prime(index), msg='{} should not be determined to be prime'.format(index))
AssertionError: True is not false : -1 should not be determined to be prime

----------------------------------------------------------------------
Ran 4 tests in 0.000s

FAILED (failures=1)

妥善地修复代码

我们看到,失败的负数是第一个数字:-1。为了解决这个问题,我们可以为负数增再增加一个特殊检查,但是编写单元测试的目的不是盲目的添加代码来检测边界情况。当一个测试失败时,我们应该退后一步并且确定解决问题的最佳方式。在这种情况下,我们就不该增加一个额外的if:
 

def is_prime(number):
  """Return True if *number* is prime."""
  if number < 0:
    return False

  if number in (0, 1):
    return False

  for element in range(2, number):
    if number % element == 0:
      return False

  return True

应当首先使用如下代码:
 

def is_prime(number):
  """Return True if *number* is prime."""
  if number <= 1:
    return False

  for element in range(2, number):
    if number % element == 0:
      return False

  return True

在后一个代码中,我们发现如果参数小于等于1时,两个if语句可以合并到一个返回值为false的语句中。这样做不仅更加简洁,并且很好的贴合了素数的定义(一个比1大并且只能被1和它本身整除的数)。

第三方测试框架

我们本来也可以通过使用第三方测试框架解决这个由于信息太少导致测试失败的问题。最常用的两个是py.test和nose。通过运行语句py.test -l(-l为显示局部变量的值)可以得到如下结果。
 

#! bash

py.test -l test_primes.py
============================= test session starts ==============================
platform linux2 -- Python 2.7.6 -- pytest-2.4.2
collected 4 items

test_primes.py ...F

=================================== FAILURES ===================================
_____________________ PrimesTestCase.test_negative_number ______________________

self = <test_primes.PrimesTestCase testMethod=test_negative_number>

  def test_negative_number(self):
    """Is a negative number correctly determined not to be prime?"""
    for index in range(-1, -10, -1):
>      self.assertFalse(is_prime(index))
E      AssertionError: True is not false

index   = -1
self    = <test_primes.PrimesTestCase testMethod=test_negative_number>

test_primes.py:22: AssertionError

正如你所看到的,一些更有用的信息。这些框架提供了比单纯的更详细的输出更多的功能,但问题是仅仅知道它们能存在和扩展内置unittest测试包的功能。

结束语

在这篇文章中,你学到了什么是单元测试,为什么它们如此重要,还有怎样编写测试。这就是说,要注意我们只是剖开了测试方法学中的表层,更多高级的话题,比如测试案例的组织、持续整合以及测试案例的管理等都是可供那些想要进一步学习Python中的测试的读者研究的很好的话题。

  •     在不改变其功能的前提下重组/清理代码
  •     编代码时不暴露其内部数据或函数并且不使用其他代码的内部数据或函数

以上是小编为您精心准备的的内容,在的博客、问答、公众号、人物、课程等栏目也有的相关内容,欢迎继续使用右上角搜索按钮进行搜索python
debug
python编程过程、c 编程调试秘笈、c 编程调试秘笈 pdf、工业机器人编程与调试、串口调试助手编程,以便于您获取更多的相关知识。

时间: 2024-09-19 09:17:18

在Python编程过程中用单元测试法调试代码的介绍_python的相关文章

《Python之禅》中对于Python编程过程中的一些建议_python

围绕一门语言,学习它的文化精髓,能让你成为一名更优秀的程序员.如果你还没读过Python之禅(Zen of Python) ,那么打开Python的命令提示符输入import this,列表中的每一项你都可以在这里找到相对应的例子. 吸引我注意力的一条是: 优雅胜于丑陋 (Beautiful is better than ugly) 看下面例子: 一个带有数字参数的list函数其功能是返回参数中的奇数可以分开写:   #--------------------------------------

对Python新手编程过程中如何规避一些常见问题的建议_python

这篇文章收集了我在Python新手开发者写的代码中所见到的不规范但偶尔又很微妙的问题.本文的目的是为了帮助那些新手开发者渡过写出丑陋的Python代码的阶段.为了照顾目标读者,本文做了一些简化(例如:在讨论迭代器的时候忽略了生成器和强大的迭代工具itertools). 对于那些新手开发者,总有一些使用反模式的理由,我已经尝试在可能的地方给出了这些理由.但通常这些反模式会造成代码缺乏可读性.更容易出bug且不符合Python的代码风格.如果你想要寻找更多的相关介绍资料,我极力推荐The Pytho

分析Python编程时利用wxPython来支持多线程的方法_python

如果你经常使用python开发GUI程序的话,那么就知道,有时你需要很长时间来执行一个任务.当然,如果你使用命令行程序来做的话,你回非常惊讶.大部分情况下,这会堵塞GUI的事件循环,用户会看到程序卡死.如何才能避免这种情况呢?当然是利用线程或进程了!本文,我们将探索如何使用wxPython和theading模块来实现. wxpython线程安全方法 wxPython中,有三个"线程安全"的函数.如果你在更新UI界面时,三个函数都不使用,那么你可能会遇到奇怪的问题.有时GUI也忙运行挺正

Python 编程中字符串插入最快的方式介绍

在 MapReduce 分布式计算时有这样一种场景:mapper 输入来自多个不同的数据源,共同点是每行记录第一列是作为 key 的 id 列,reducer 需要根据数据源的不同,进行相应的处理.由于数据到 reducer 阶段已经无法区分来自什么文件,所以一般采取的方法是 mapper 为数据记录打一个 TAG.为了便于使用,我习惯于把这个 TAG 打到数据的第二列(第一列为 id 列,作为 reduce/join 的 key),所以有这样的 mapper 函数: def mapper1(l

用Python解析XML的几种常见方法的介绍_python

一.简介        XML(eXtensible Markup Language)指可扩展标记语言,被设计用来传输和存储数据,已经日趋成为当前许多新生技术的核心,在不同的领域都有着不同的应用.它是web发展到一定阶段的必然产物,既具有SGML的核心特征,又有着HTML的简单特性,还具有明确和结构良好等许多新的特性.         python解析XML常见的有三种方法:一是xml.dom.*模块,它是W3C DOM API的实现,若需要处理DOM API则该模块很适合,注意xml.dom包

Python最基本的数据类型以及对元组的介绍_python

简单类型 内置到 Python 编程语言中的简单数据类型包括:     bool     int     float     complex 支持简单数据类型不是 Python 独有的功能,因为多数现代编程语言都具有完整类型补充.例如 Java? 语言甚至有一组更丰富的原始数据类型:     byte     short     int     long     float     double     char     boolean 但是,在 Python 中,简单数据类型并不是原始数据类型

Python中条件选择和循环语句使用方法介绍_python

同C语言.Java一样,Python中也存在条件选择和循环语句,其风格和C语言.java的很类似,但是在写法和用法上还是有一些区别.今天就让我们一起来了解一下. 一.条件选择语句 Python中条件选择语句的关键字为:if .elif .else这三个.其基本形式如下: 复制代码 代码如下: if condition: block elif condition: block ... else block 其中elif和else语句块是可选的.对于if和elif只有condition为True时,

Python中几种操作字符串的方法的介绍_python

#! -*- coding:utf-8 -*- import string s = 'Yes! This is a string' print '原字符串:' + s print '小写:' + s.lower() print '大写:' + s.upper() print '大小写转换:' + s.swapcase() print '首字母大写:' + s.capitalize() print '每个单词首字母大写:' + s.title() #各种对齐函数 print '左对齐:' + s.

python基础教程之基本数据类型和变量声明介绍_python

变量不需要声明 Python的变量不需要声明,你可以直接输入: 复制代码 代码如下: >>>a = 10 那么你的内存里就有了一个变量a, 它的值是10,它的类型是integer (整数). 在此之前你不需要做什么特别的声明,而数据类型是Python自动决定的. 复制代码 代码如下: >>>print a >>>print type(a) 那么会有如下输出: 复制代码 代码如下: 10 <type 'int'> 这里,我们学到一个内置函数t