《Python Testing Cookbook》读书笔记之一:单元测试


Python Testing Cookbook

读书笔记 pythontesting

Chapter 1: Using Unittest To Develop Basic Tests

配置虚拟环境

在开始写代码测试前,先创建一个独立的测试开发环境,这样可以避免各种包和现有开发环境互相影响,适合进行测试。

一般可以通过virtualenv来创建虚拟环境,这里是官方文档和一篇写得比较好的中文版指南。如果你和我一样使用Anaconda的Python发行版的话,可以使用conda create命令来进行操作,指南戳这里

Anaconda 是一个用来进行大规模数据处理,预测分析和科学计算的Python发行包,里面内置了iPython,NumPy,SciPy等近200种常用包,如果你用python用来做这些事情比较多的话,建议可以直接下载这个。官方地址:https://store.continuum.io/cshop/anaconda/

Asserting the basics

使用例子:

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,并继承unittest类,如ClassXxxTest(unittest.TestCase)
    2. 在测试类中建立测试方法,如test_xxx_xxx方法
    3. 在类外import unittest,并在主程序入口执行unittest.main()
  • 基本的assert语句:

assertEquals(first, second[, msg])
assertTrue(expression[, msg])
assertFalse(expression[, msg])
assertRaises(exception, callable, ...)
  • 尽量使用assertEquals语句而非assertTrueassertFalse,因为其他几个只会报错,而assertEquals可以显示两个值分别是多少,提供了更多的信息。

  • Unittest可以使用self.fail([msg])来产生失败测试,但尽量使用assert语句改写,因为这个语句在测试正确的情况下用不到。

使用setUptearDown函数在测试前后进行有关处理

如需要每个测试都需要新建实例进行初始化操作的话,可以定义在setUp中;需要在测试中打开文件的话,在tearDown中使用close方法。

将测试类打包成test suite进行测试

if __name__ == "__main__":
    suite = unittest.TestLoader().loadTestsFromTestCase( \
                    RomanNumeralConverterTest)
    unittest.TextTestRunner(verbosity=2).run(suite)

在测试方法中插入注释信息,在每一次该方法运行和失败时显示

def test_parsing_century(self):
    "This test method is coded to fail for demo."
    value = RomanNumeralConverter("C")
    self.assertEquals(10, value.convert_to_decimal())

Alt text

运行一部分测试用例

当测试例子变得很多的时候,每一次都全部运行需要花费很长的时间,此时我们可以用这个方法运行一部分测试用例。

if __name__ == "__main__":
  import sys
  suite = unittest.TestSuite()
  if len(sys.argv) == 1:
    suite = unittest.TestLoader().loadTestsFromTestCase(\
                  RomanNumeralConverterTest)
  else:
    for test_name in sys.argv[1:]:
      suite.addTest(\
        RomanNumeralConverterTest(test_name))
  unittest.TextTestRunner(verbosity=2).run(suite)

用独立的test文件测试几个test suite

这是将几个test suite都放在主程序中

 if __name__ == "__main__":
     import unittest
     from recipe5 import *
     suite1 = unittest.TestLoader().loadTestsFromTestCase( \
                    RomanNumeralConverterTest)
     suite2 = unittest.TestLoader().loadTestsFromTestCase( \
                    RomanNumeralComboTest)
     suite = unittest.TestSuite([suite1, suite2])
     unittest.TextTestRunner(verbosity=2).run(suite)

还可以将几个不同的test suite定义在测试模块中

def combos():
  return unittest.TestSuite(map(RomanNumeralConverterTest,\
       ["test_combo1", "test_combo2", "test_combo3"]))
def all():
  return unittest.TestLoader().loadTestsFromTestCase(\
                RomanNumeralConverterTest)

用以下方法调用所有的suite,如需调用某一个或某几个,参照修改即可。不同的suite可以用来实现不同功能的测试。

if __name__ == "__main__":
    for suite_func in [combos, all]:
       print "Running test suite '%s'" % suite_func.func_name
       suite = suite_func()
       unittest.TextTestRunner(verbosity=2).run(suite)

将老的assert测试代码改成单元测试代码

这是老的测试类

class RomanNumeralTester(object):
  def __init__(self):
    self.cvt = RomanNumeralConverter()
  def simple_test(self):
    print "+++ Converting M to 1000"
    assert self.cvt.convert_to_decimal("M") == 1000

通过unittest.FunctionTestCase方法将其转换成unittest方法,然后添加到suite里。传统的assert方法在一个assert失败后就会报错退出,改成这种形式后,会将所有的测试用例都测试后才退出,并展示错误信息。

import unittest
if __name__ == "__main__":
    tester = RomanNumeralTester()
    suite = unittest.TestSuite()
    for test in [tester.simple_test, tester.combo_test1, \
            tester.combo_test2, tester.other_test]:
        testcase = unittest.FunctionTestCase(test)
        suite.addTest(testcase)
    unittest.TextTestRunner(verbosity=2).run(suite)

将有多个assertion的复杂测试方法拆散成每次测试一个简单功能的小测试方法

def test_convert_to_decimal(self):
    self.assertEquals(0, self.cvt.convert_to_decimal(""))
    self.assertEquals(1, self.cvt.convert_to_decimal("I"))
    self.assertEquals(2010, self.cvt.convert_to_decimal("MMX"))
    self.assertEquals(4000, self.cvt.convert_to_decimal("MMMM"))

应该写成

def test_to_decimal1(self):
    self.assertEquals(0, self.cvt.convert_to_decimal(""))
def test_to_decimal2(self):
    self.assertEquals(1, self.cvt.convert_to_decimal("I"))
def test_to_decimal3(self):
    self.assertEquals(2010, self.cvt.convert_to_decimal("MMX"))
def test_to_decimal4(self):
    self.assertEquals(4000, self.cvt.convert_to_decimal("MMMM"))

这样的好处是前者发生错误时只会报一个错,且第一个assert语句出错时不会执行后面的测试,而第二种方法会检测所有用例,并给出详细的错误统计。

如果我们有很多组要测试的值,这里面会出现大量的重复代码,有没有简单点儿的方法呢?我们可以手动改变python的命名空间实现批量加入函数.

先来看一段代码:

def make_add(n):
    def func(x):
        return x+n
    return func

if __name__ == '__main__':
    for i in xrange(1,10):
        locals()['add_%d'%i] = make_add(i)
    print add_1(7)
    print add_9(19)

在python中函数可以作为参数传递,所以make_add方法可以生成一个加n的函数返回。而在主程序的循环里,我们将add_n方法通过make_add函数来生成,再通过加入locals()添加到本地命名空间,这相当于在本地创建了从add_1add_9的9个函数。

这个搞明白后,就可以动手改写前面的代码了。

v_s = [
    (1000, "M"),
    (100, "C"),
    (50, "L"),
    (10, "X"),
    (5, "V"),
    (1, "I"),
]
def make_test(v, s):
    def func(self):
        value = RomanNumeralConverter(s)
        self.assertEquals(v, value.convert_to_decimal())
    return func
for for i, (j, k) in enumerate(v_s, 1):
    locals()['test_to_decimal%d' % i] = make_test(v, s)

这段代码可以实现前面第二种写法的功能,当你想要添加新的测试用例时,只需在列表v_s中添加即可。

通过迭代实现批量测试

当测试用例很多的时候,还可以使用下面的方法实现批量添加。我们自己写了一个生成assert语句的函数。但这种情况类似于上面讲过的第一种方法,即将很多assert语句写在了同一个测试函数中,如果有一个发生错误,它后面的例子都不会被测试。

def test_bad_inputs(self):
    r = self.cvt.convert_to_roman
    d = self.cvt.convert_to_decimal
    edges = [("equals", r, "", None),\
             ("equals", r, "I", 1.2),\
             ("raises", d, TypeError, None),\
             ("raises", d, TypeError, 1.2)\
            ]

    [self.checkout_edge(edge) for edge in edges]

def checkout_edge(self, edge):
    if edge[0] == "equals":
        f, output, input = edge[1], edge[2], edge[3]
        print("Converting %s to %s..." % (input, output))
        self.assertEquals(output, f(input))
    elif edge[0] == "raises":
        f, exception, args = edge[1], edge[2], edge[3:]
        print("Converting %s, expecting %s" % \
                                       (args, exception))
        self.assertRaises(exception, f, *args)
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章