《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)
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章