Python单元测试

Table of Contents

1 测试驱动开发

以测试来驱动代码开发的过程,讲究测试先行,然后再实现功能,这样做的好处:

  • 在长期实施测试驱动开发,我们可以明显感受到养成了良好的编码风格,函数与类的单一职责更是自然行成;
  • 更加自信地修补bug和重构代码。尤其是在代码变多的情况下,没有单元测试就会经常出现改好这里,那里又出问题。测试用例可以更好的避免这种情况;
  • 在我的实践中,安全相关的也要写入测试用例,这样能更好保证代码的安全性。review安全相关的测试用例时也能发现哪些安全问题没有覆盖到。

整个实践的过程很简单:

1、测试先行,写好功能测试(从用户角度去写)

2、运行测试用例,失败以后再写功能代码,直到测试用例通过

整个过程是逐个实现的,切勿一次写多个测试用例,再写代码。

重构时,先写改测试用例,然后按上面的步骤来重构功能代码,切勿修改测试用例的同时也修改功能代码。

重要的一点:单元测试是用来测试流程和代码逻辑的,无关的东西不要测试(比如常量)。

下面部分来源于《Python Testing Cookbook》笔记

2 基本的断言

有这样一个类代码:

class RomanNumeralConverter(object):
    def __init__(self, roman_numeral):
	self.roman_numeral = roman_numeral
	self.digit_map = {"M":1000, "D":500, "C":100, "L":50, "X":10, "V":5, "I":1}

    def convert_to_decimal(self):
	val = 0

	for char in self.roman_numeral:
	    val += self.digit_map[char]

	return val

该类完成字符到值的映射,为了测试正确性,创建以下测试代码:

import unittest

class RomanNumeralConverterTest(unittest.TestCase):
    def test_parsing_millenia(self):
	value = RomanNumeralConverter("M")
	self.assertEquals(1000, value.convert_to_decimal())

    def test_parsing_century(self):
	value = RomanNumeralConverter("C")
	self.assertEquals(100, value.convert_to_decimal())

    def test_parsing_half_century(self):
	value = RomanNumeralConverter("L")
	self.assertEquals(50, value.convert_to_decimal())

    def test_parsing_decade(self):
	value = RomanNumeralConverter("X")
	self.assertEquals(10, value.convert_to_decimal())

    def test_parsing_half_decade(self):
	value = RomanNumeralConverter("V")
	self.assertEquals(5, value.convert_to_decimal())

    def test_parsing_one(self):
	value = RomanNumeralConverter("I")
	self.assertEquals(1, value.convert_to_decimal())

    def test_empty_roman_numeral(self):
	value = RomanNumeralConverter("")
	self.assertTrue(value.convert_to_decimal() == 0)
	self.assertFalse(value.convert_to_decimal() > 0)

    def test_no_roman_numeral(self):
	value = RomanNumeralConverter(None)
	self.assertRaises(TypeError, value.convert_to_decimal)

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

说明:

  1. 创建一个Test结尾的类名RomanNumeralConverterTest
  2. 测试类继承unittest.TestCase类
  3. 每个测试用例为一个方法,以test开头
  4. 执行unittest.main()函数完成自动测试

3 setUp和tearDown

修改下RomanNumeralConverter代码:

class RomanNumeralConverter(object):
    def __init__(self):
	self.digit_map = {"M":1000, "D":500, "C":100, "L":50, "X":10, "V":5, "I":1}

    def convert_to_decimal(self, roman_numeral):
	val = 0

	for char in roman_numeral:
	    val += self.digit_map[char]

	return val

改动在于将构造类中romannumeral参数放到了converttodecimal方法中。现在重新写测试用例:

class RomanNumeralConverterTest(unittest.TestCase):
    def setUp(self):
	print 'Creating a new RomanNumeralConverter'
	self.value = RomanNumeralConverter()

    def tearDown(self):
	print 'Destroying the RomanNumeralConverter'
	self.value = None

    def test_parsing_millenia(self):
	self.assertEquals(1000, self.value.convert_to_decimal('M'))

    def test_parsing_century(self):
	self.assertEquals(100, self.value.convert_to_decimal('C'))

    def test_parsing_half_century(self):
	self.assertEquals(50, self.value.convert_to_decimal('L'))

    def test_parsing_decade(self):
	self.assertEquals(10, self.value.convert_to_decimal('X'))

    def test_parsing_half_decade(self):
	self.assertEquals(5, self.value.convert_to_decimal('V'))

    def test_parsing_one(self):
	self.assertEquals(1, self.value.convert_to_decimal('I'))

    def test_empty_roman_numeral(self):
	self.assertTrue(self.value.convert_to_decimal('') == 0)
	self.assertFalse(self.value.convert_to_decimal('') > 0)

    def test_no_roman_numeral(self):
	self.assertRaises(TypeError, self.value.convert_to_decimal, None)

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

setUp和tearDown跟类的构造函数和析构函数是一样的,setUp用于测试前做环境初始化,如要测试数据库,则在setUp中需要先行连接数据库;tearDown则是销毁环境作用,如关闭数据库。

4 Mock测试

在普通的单元测试中,测试的很多东西都是不依赖环境的,比如某个算法;实际情况中,有很多函数还要完成HTTP请求、数据库连接等依赖具体环境的。

假如要测试一个数据库连接,如果没有连接成功,返回None,否则返回具体对象。普通的单元测试要完成,就需要先连接好数据库,然后跑一次测试用例,然后再断开数据库,又跑一次。

Mock测试就可以直接“模拟”出这些“环境”。

4.1 Mock、Stub的区别

个人理解是这样的:

Mock测试通过虚拟出类对象,而是实实在在的虚拟,没有具体的实现代码 Stub也是虚拟出的对象,可是这个“虚拟”却要自己去实现代码了。比如要测试某个类的某个方法,可以实现一个继承类,然后覆盖父类的方法。

Stub有具体实现的方法,所以有时候看别人写的测试时,还得去看看它是如何实现这个虚拟对象的。

对于Mock,我们期望测试中模拟出来的方法有没有调用、调用的次数,甚至调用顺序以及参数等;Stub是没有这些实现的(当然可以自己实现,那不是太复杂了么)。

4.2 mock库

Python里现成的mock库,使用pip安装:

sudo pip install mock

4.3 简单实例

有这么个函数:

import pymongo

def connection():
    return pymongo.Connection()

我们期待它在连接不成功的时候返回None,要测试的话,直接用这段代码:

from mock import MagicMock
pymongo.Connection = MagicMock(return_value=None)
conn = connection()
# 如果返回不是None
assert(not conn is None)

可看出,Mock其实就是虚拟了一个对象而已。

5 测试覆盖率

Coverage.py用于统计单元测试的测试覆盖率的工具。

运行单个测试用例脚本:

$ coverage run test/test_app_browser.py

然后生成覆盖率统计报告:

$ coverage report
Name                                                                                                               Stmts   Miss  Cover
--------------------------------------------------------------------------------------------------------------------------------------
app/__init__.py                                                                                                       47      0   100%
app/api.py                                                                                                           195    150    23%
app/browser/__init__.py                                                                                                0      0   100%

输出的各列含义:

Stmts:有效代码总行数(没有算注释、空行)

Miss:未执行的代码总行数(没有算注释、空行)

Cover:覆盖率

如果用的pytest,建议使用pytest-cov这个工具,它封装了Coverage。使用方式也简单:

$ py.test -v --cov=app 单元测试目录

注意–cov参数,它用于过滤所属项目的测试结果,如果没有过滤的话,每个执行过的模块都被打印出来。