Python Unittest

Startup

單元測(cè)試的核心價(jià)值在于兩點(diǎn):

  1. 更加精確地定義某段代碼的作用洪添,從而使代碼的耦合性更低
  2. 避免程序員寫出不符合預(yù)期的代碼,以及因新增功能而帶來(lái)的Regression Bug

隨著Test-Driven方法論的流行笨蚁,測(cè)試類庫(kù)對(duì)于高級(jí)語(yǔ)言來(lái)說(shuō)變得不可或缺坪圾。Python生態(tài)圈中的unit testing framework相當(dāng)多垮衷,不同于Java幾乎只有JUnit與TestNG二選一展蒂,Python unittest框架中較為活躍并也有較多使用者的framework就有unittest、unittest2苔咪、nose锰悼、nose2與py.test等。不計(jì)其他較小眾的工具团赏,光是要搞懂這些工具并從中挑選一個(gè)合適的出來(lái)使用就讓人頭大了箕般。本文因此總結(jié)了這些類庫(kù)在實(shí)戰(zhàn)中的作用,以便讀者在選擇時(shí)方便比對(duì)參考舔清。

這里是介紹Python測(cè)試的官方文檔:

本文在該文檔的基礎(chǔ)上刪減了入門部分丝里,增加了深入講解和實(shí)戰(zhàn)案例。

類庫(kù)

Unittest

Unittest的標(biāo)準(zhǔn)文檔在這里:

  1. Python2
  2. Python3

Unittest是Python標(biāo)準(zhǔn)庫(kù)的一部分体谒。它是目前最流行的固件測(cè)試框架XUnit在Python中的實(shí)現(xiàn)杯聚,如果你接觸過(guò)Junit,nUnit抒痒,或者CppUnit幌绍,你會(huì)非常熟悉它的API。

Unittest框架的單元測(cè)試類用例通過(guò)繼承unittest.TestCase來(lái)實(shí)現(xiàn)故响,看起來(lái)像是這樣:

import unittest

def fun(x):
    return x + 1

class MyTest(unittest.TestCase):
    def test(self):
        self.assertEqual(fun(3), 4)

Unittest一共包含4個(gè)概念:

  1. Test Fixture傀广,就是Setup()和TearDown()
  2. Test Case,一個(gè)Test Case就是一個(gè)測(cè)試用例彩届,他們都是unittest.TestCase類的子類的方法
  3. Test Suite伪冰,Test Suite是一個(gè)測(cè)試用例集合,基本上你用不到它樟蠕,用unittest.main()或者其它發(fā)現(xiàn)機(jī)制來(lái)運(yùn)行所有的測(cè)試用例就對(duì)了贮聂。 :)
  4. Test runner,這是單元測(cè)試結(jié)果的呈現(xiàn)接口寨辩,你可以定制自己喜歡的呈現(xiàn)方式寂汇,比如GUI界面,基本上你也用不到它捣染。

一些實(shí)戰(zhàn)中需要用到的技巧:

  • 發(fā)現(xiàn)機(jī)制

      python -m unittest discover -s Project/Test/Directory -p "*test*"
      
      # 等同于
      python -m unittest discover -s Project/Test/Directory
    
  • 用Assert骄瓣,不要用FailUnless(它們已經(jīng)被廢棄)

    Deprecated.png
  • 常用的Assert

    NormalAssert.png
  • 特殊的Assert

    SpecificAssert.png

    For example:

      assertAlmostEqual(1.1, 3.3-2.15, places=1)
    
    SpecificEqual.png
  • AssertException

    AssertException.png
    • assertRaises

      1. assertRaises(exception, callable, *args, **kwds)

         def raisesIOError(*args, **kwds):
             raise IOError("TestIOError")
         
         class FixtureTest(unittest.TestCase):
             def test1(self):
                 self.asertRaises(IOError, raisesIOError)
         
         if __name__ == '__main__':
             unittest.main()
        
      2. assertRaises(exception)

         # If only the exception argument is given,
         # returns a context manager so that the code 
         # under test can be written inline rather 
         # than as a function
         with self.assertRaises(SomeException):
             do_something()
             
         # The context manager will store the caught 
         # exception object in its exception attribute. 
         # This can be useful if the intention is to 
         # perform additional checks on the exception raised
         with self.assertRaises(SomeException) as cm:
             do_something()
         
         the_exception = cm.exception
         self.assertEqual(the_exception.error_code, 3)
        
    • assertRaisesRegexp

        self.assertRaisesRegexp(ValueError, "invalid literal for.*XYZ'$", int, 'XYZ')
      
        # or
      
        with self.assertRaisesRegexp(ValueError, 'literal'):
            int('XYZ')
      
  • Skip,出于各種原因,你可能需要暫時(shí)跳過(guò)一些測(cè)試用例(而不是刪除它們)

      class MyTestCase(unittest.TestCase):
      
          @unittest.skip("demonstrating skipping")
          def test_nothing(self):
              self.fail("shouldn't happen")
          
          @unittest.skipIf(mylib.__version__ < (1, 3),
                           "not supported in this library version")
          def test_format(self):
              # Tests that work for only a certain version of the library.
              pass
          
          @unittest.skipUnless(sys.platform.startswith("win"), "requires Windows")
          def test_windows_support(self):
              # windows specific testing code
              pass
    
  • Class level fixtures

      import unittest
    
      class Test(unittest.TestCase):
          @classmethod
          def setUpClass(cls):
              cls._connection = createExpensiveConnectionObject()
      
          @classmethod
          def tearDownClass(cls):
              cls._connection.destroy()
    
  • Module level fixtures

      # These should be implemented as functions:
      
      def setUpModule():
          createConnection()
      
      def tearDownModule():
          closeConnection()
    

Mock

Mock類庫(kù)是一個(gè)專門用于在unittest過(guò)程中制作(偽造)和修改(篡改)測(cè)試對(duì)象的類庫(kù)榕栏,制作和修改的目的是避免這些對(duì)象在單元測(cè)試過(guò)程中依賴外部資源(網(wǎng)絡(luò)資源畔勤,數(shù)據(jù)庫(kù)連接,其它服務(wù)以及耗時(shí)過(guò)長(zhǎng)等)扒磁。Mock是一個(gè)如此重要的類庫(kù)庆揪,如果沒(méi)有它,Unittest框架從功能上來(lái)說(shuō)就是不完整的妨托。所以不能理解為何它沒(méi)有出現(xiàn)在Python2的標(biāo)準(zhǔn)庫(kù)里缸榛,不過(guò)我們可以很高興地看到在Python3中mock已經(jīng)是unittest框架的一部分。

  • 猴子補(bǔ)丁兰伤,Monkey-patching is the technique of swapping functions or methods with others in order to change a module, library or class behavior.

      >>> class Class():
      ...    def add(self, x, y):
      ...       return x + y
      ...
      >>> inst = Class()
      >>> def not_exactly_add(self, x, y):
      ...    return x * y
      ...
      >>> Class.add = not_exactly_add
      >>> inst.add(3, 3)
      9
    
  • Mock對(duì)象

    • return_value: 設(shè)置Mock方法的返回值

        >>> from mock import Mock
        >>> class ProductionClass(): pass
        ... 
        >>> real = ProductionClass()
        >>> real.method = Mock(return_value=3)
        >>> real.method(3, 4, 5, key='value')
        3
        >>> real.method.assert_called_with(3, 4, 5, key='value')
        >>> real.method.assert_called_with(3, 4, key='value')
        Traceback (most recent call last):
          File "<stdin>", line 1, in <module>
          File "/usr/local/lib/python2.7/site-packages/mock/mock.py", line 937, in assert_called_with
            six.raise_from(AssertionError(_error_message(cause)), cause)
          File "/usr/local/lib/python2.7/site-packages/six.py", line 718, in raise_from
            raise value
        AssertionError: Expected call: mock(3, 4, key='value')
        Actual call: mock(3, 4, 5, key='value')
      
    • side_effect:

      • 調(diào)用Mock方法時(shí)内颗,拋出異常

          >>> mock = Mock(side_effect=KeyError('foo'))
          >>> mock()
          Traceback (most recent call last):
           ...
          KeyError: 'foo'
        
          >>> mock = Mock()
          >>> mock.return_value = 42
          >>> mock()
          42
        
      • 調(diào)用Mock方法時(shí),根據(jù)參數(shù)得到不同的返回值

          >>> values = {'a': 1, 'b': 2, 'c': 3}
          >>> def side_effect(arg):
          ...     return values[arg]
          ...
          >>> mock.side_effect = side_effect
          >>> mock('a'), mock('b'), mock('c')
          (1, 2, 3)
        
      • 模擬生成器

          >>> mock.side_effect = [5, 4, 3, 2, 1]
          >>> mock()
          5
          >>> mock(), mock(), mock(), mock()
          (4, 3, 2, 1)
          >>> mock()
          Traceback (most recent call last):
            File "<stdin>", line 1, in <module>
            File "/usr/local/lib/python2.7/site-packages/mock/mock.py", line 1062, in __call__
              return _mock_self._mock_call(*args, **kwargs)
            File "/usr/local/lib/python2.7/site-packages/mock/mock.py", line 1121, in _mock_call
              result = next(effect)
            File "/usr/local/lib/python2.7/site-packages/mock/mock.py", line 109, in next
              return _next(obj)
          StopIteration
        
    • patch:在函數(shù)(function)或者環(huán)境管理協(xié)議(with)中模擬對(duì)象敦腔,離開(kāi)函數(shù)或者環(huán)境管理器范圍后模擬行為結(jié)束均澳。

      • 在函數(shù)中

          from mock import patch
          
          class AClass(object): pass
          class BClass(object): pass
          
          print id(AClass), id(BClass)
          
          @patch('__main__.AClass')
          @patch('__main__.BClass')
          def test(x, MockClass2, MockClass1):
              print id(AClass), id(BClass)
              print id(MockClass1), id(MockClass2)
              print AClass
              print AClass()
              assert MockClass1.called
              print x
          
          test(42)
          
          # output:
          """
          140254580491744 140254580492688
          4525648336 4517777552
          4525648336 4517777552
          <MagicMock name='AClass' id='4525648336'>
          <MagicMock name='AClass()' id='4525719824'>
          42
          """
        
      • 在環(huán)境管理協(xié)議中

          >>> class Class(object):
          ...     def method(self):
          ...         pass
          ...
          >>> with patch('__main__.Class') as MockClass:
          ...     instance = MockClass.return_value
          ...     instance.method.return_value = 'foo'
          ...     assert Class() is instance
          ...     assert Class().method() == 'foo'
          ...
        
      • Spec Set的寫法,你應(yīng)該用不到

          >>> Original = Class
          >>> patcher = patch('__main__.Class', spec=True)
          >>> MockClass = patcher.start()
          >>> instance = MockClass()
          >>> assert isinstance(instance, Original)
          >>> patcher.stop()
        
    • patch.object: 在函數(shù)或者環(huán)境管理協(xié)議中模擬對(duì)象符衔,但只mock其中一個(gè)attribute

        from mock import patch
        
        class AClass():
            def method(self, *arg):
                return 42
        
        with patch.object(AClass, 'method') as mock_method:
            mock_method.return_value = "Fake"
            real = AClass()
            print real.method(1, 2, 3)   # Fake
        
        mock_method.assert_called_once_with(1, 2, 3)
        print real.method(1, 2, 3) # 42
      
    • patch.dict: patch.dict can be used to add members to a dictionary, or simply let a test change a dictionary, and ensure the dictionary is restored when the test ends.

      patch.dict(in_dict, values=(), clear=False, **kwargs)

      If clear is True then the dictionary will be cleared before the new values are set.

        >>> from mock import patch
        >>> foo = {}
        >>> with patch.dict(foo, {'newkey': 'newvalue'}):
        ...     assert foo == {'newkey': 'newvalue'}
        ...
        >>> assert foo == {}
        
        >>> import os
        >>> with patch.dict('os.environ', {'newkey': 'newvalue'}):
        ...     print os.environ['newkey']
        ...
        newvalue
        >>> assert 'newkey' not in os.environ

- patch.multiple: Perform multiple patches in a single call.

        >>> thing = object()
        >>> other = object()
        
        >>> @patch.multiple('__main__', thing=DEFAULT, other=DEFAULT)
        ... def test_function(thing, other):
        ...     assert isinstance(thing, MagicMock)
        ...     assert isinstance(other, MagicMock)
        ...
        >>> test_function()
        
        >>> @patch('sys.exit')
        ... @patch.multiple('__main__', thing=DEFAULT, other=DEFAULT)
        ... def test_function(mock_exit, other, thing):
        ...     assert 'other' in repr(other)
        ...     assert 'thing' in repr(thing)
        ...     assert 'exit' in repr(mock_exit)
        ...
        >>> test_function()
  • MagicMock: MagicMock是Mock的子類找前。MagicMock is a subclass of Mock with default implementations of most of the magic methods. You can use MagicMock without having to configure the magic methods yourself.

    • MagicMock的功能完全cover Mock,比如:

        from mock import MagicMock
        thing = ProductionClass()
        thing.method = MagicMock(return_value=3)
        thing.method(3, 4, 5, key='value')  # return 3
        
        thing.method.assert_called_with(3, 4, 5, key='value')
      
    • MagicMock相對(duì)于Mock的優(yōu)勢(shì):

        >>> from mock import MagicMock
        >>> mock = MagicMock()
        >>> mock.__str__.return_value = 'foobarbaz'
        >>> str(mock)
        'foobarbaz'
        >>> mock.__str__.assert_called_once_with()
      

      原來(lái)需要:

        >>> from mock import Mock
        >>> mock = Mock()
        >>> mock.__str__ = Mock(return_value = 'wheeeeee')
        >>> str(mock)
        'wheeeeee'
      
  • create_autospec: 使Mock對(duì)象擁有和原對(duì)象相同的字段和方法判族,對(duì)于方法對(duì)象躺盛,則擁有相同的簽名

      >>> from mock import create_autospec
      >>> def function(a, b, c):
      ...     pass
      ...
      >>> mock_function = create_autospec(function, return_value='fishy')
      >>> mock_function(1, 2, 3)
      'fishy'
      >>> mock_function.assert_called_once_with(1, 2, 3)
      >>> mock_function('wrong arguments')
      Traceback (most recent call last):
       ...
      TypeError: <lambda>() takes exactly 3 arguments (1 given)
      
      >>> from mock import create_autospec
      >>> 
      >>> mockStr = create_autospec(str)
      >>> print mockStr.__add__("d", "e")
      <MagicMock name='mock.__add__()' id='4537473552'>
    

Unittest2

Unittest2致力于將Python2.7及以后版本上unittest框架的新特性移植(backport)到Python2.4~Python2.6平臺(tái)中。

Backport是將一個(gè)軟件補(bǔ)丁應(yīng)用到比該補(bǔ)丁所對(duì)應(yīng)的版本更老的版本的行為形帮。

你知道這些就可以了颗品,基本上你不會(huì)用到它

The new features in unittest backported to Python 2.4+. unittest2 is a backport of the new features added to the unittest testing framework in Python 2.7 and onwards.

unittest2 is a backport of Python 2.7’s unittest module which has an improved API and better assertions over the one available in previous versions of Python.

unittest2py3k is the Python 3 compatible version of unittest2

py.test

pytest是另一種固件測(cè)試框架沃缘,它的API設(shè)計(jì)非常簡(jiǎn)潔優(yōu)雅躯枢,完全脫離了XUnit的窠臼(unittest是XUnit在Python中的實(shí)現(xiàn))。但這也正是它的缺點(diǎn)槐臀,unittest是標(biāo)準(zhǔn)庫(kù)的一部分锄蹂,用者甚眾,與之大異難免曲高和寡水慨。

py.test功能完備得糜,并且可擴(kuò)展,但是它語(yǔ)法很簡(jiǎn)單晰洒。創(chuàng)建一個(gè)測(cè)試組件和寫一個(gè)帶有諸多函數(shù)的模塊一樣容易朝抖,來(lái)看一個(gè)例子

# content of test_sample.py
def func(x):
    return x + 1

def test_answer():
    assert func(3) == 5

運(yùn)行一下:

$ py.test 
============= test session starts =============
platform darwin -- Python 2.7.11, pytest-2.9.2, py-1.4.31, pluggy-0.3.1
rootdir: /Users/wuwenxiang/Documents/workspace/testPyDev, inifile: 
collected 1 items 

some_test.py F

================== FAILURES ===================
_________________ test_answer _________________

    def test_answer():
>       assert func(3) == 5
E       assert 4 == 5
E        +  where 4 = func(3)

some_test.py:6: AssertionError
========== 1 failed in 0.01 seconds ===========

官方文檔中入門的例子在這里,pytest也給出了unittest Style的兼容寫法示例谍珊,然并X治宣,看完之后你會(huì)發(fā)現(xiàn):圈子不同,不必強(qiáng)融,這句話還真TM有道理侮邀。

py.test的setup/teardown語(yǔ)法與unittest的兼容性不高坏怪,實(shí)現(xiàn)方式也不直觀。

我們來(lái)看一下setup/teardown的例子:

# some_test.py

import pytest

@pytest.fixture(scope='function')
def setup_function(request):
    def teardown_function():
        print("teardown_function called.")
    request.addfinalizer(teardown_function)
    print('setup_function called.')

@pytest.fixture(scope='module')
def setup_module(request):
    def teardown_module():
        print("teardown_module called.")
    request.addfinalizer(teardown_module)
    print('setup_module called.')


def test_1(setup_function):
    print('Test_1 called.')

def test_2(setup_module):
    print('Test_2 called.')

def test_3(setup_module):
    print('Test_3 called.')

pytest創(chuàng)建固件測(cè)試環(huán)境(fixture)的方式如上例所示绊茧,通過(guò)顯式指定scope=''參數(shù)來(lái)選擇需要使用的pytest.fixture裝飾器铝宵。即一個(gè)fixture函數(shù)的類型從你定義它的時(shí)候就確定了,這與使用@nose.with_setup()不同华畏。對(duì)于scope='function'的fixture函數(shù)鹏秋,它就是會(huì)在測(cè)試用例的前后分別調(diào)用setup/teardown。測(cè)試用例的參數(shù)如def test_1(setup_function)只負(fù)責(zé)引用具體的對(duì)象亡笑,它并不關(guān)心對(duì)方的作用域是函數(shù)級(jí)的還是模塊級(jí)的侣夷。

有效的 scope 參數(shù)限于:function, module, class, session,默認(rèn)為function况芒。

運(yùn)行上例:$ py.test some_test.py -s惜纸。-s用于顯示print()函數(shù)

執(zhí)行效果:

$ py.test -s some_test.py
============= test session starts =============
platform darwin -- Python 2.7.11, pytest-2.9.2, py-1.4.31, pluggy-0.3.1
rootdir: /Users/wuwenxiang/Documents/workspace/testPyDev, inifile: 
collected 3 items 

some_test.py setup_function called.
Test_1 called.
.teardown_function called.
setup_module called.
Test_2 called.
.Test_3 called.
.teardown_module called.


========== 3 passed in 0.01 seconds ===========

這里需要注意的地方是:setup_module被調(diào)用的位置叶撒。

Nose

nose廣為流傳绝骚,它主要用于配置和運(yùn)行各種框架下的測(cè)試用例,有更簡(jiǎn)潔友好的測(cè)試用例發(fā)現(xiàn)功能祠够。nose的自動(dòng)發(fā)現(xiàn)策略是會(huì)遍歷文件夾压汪,搜索特征文件(默認(rèn)是搜索文件名中帶test的文件)

$ nosetests
F.
======================================================================
FAIL: some_test.test_answer
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/usr/local/lib/python2.7/site-packages/nose/case.py", line 197, in runTest
    self.test(*self.arg)
  File "/Users/wuwenxiang/Documents/workspace/testPyDev/some_test.py", line 6, in test_answer
    assert func(3) == 5
AssertionError

----------------------------------------------------------------------
Ran 2 tests in 0.004s

FAILED (failures=1)

很可惜,官網(wǎng)說(shuō):Nose has been in maintenance mode for the past several years and will likely cease without a new person/team to take over maintainership. New projects should consider using Nose2, py.test, or just plain unittest/unittest2.

Nose2是Nose的原班人馬開(kāi)發(fā)古瓤。nose2 is being developed by the same people who maintain nose.
Nose2是基于unittest2 plugins分支開(kāi)發(fā)的止剖,但并不支持python2.6之前的版本。Nose2致力于做更好的Nose落君,它的Plugin API并不兼容之前Nose的API穿香,所以如果你migration from Nose,必須重寫這些plugin绎速。nose2 implements a new plugin API based on the work done by Michael Foord in unittest2’s plugins branch. This API is greatly superior to the one in nose, especially in how it allows plugins to interact with each other. But it is different enough from the API in nose that supporting nose plugins in nose2 will not be practical: plugins must be rewritten to work with nose2.

然而……
Nose2的更新……也很有限……

其作者Jason Pellerin先生坦言他目前(2014年)并沒(méi)有多余的時(shí)間進(jìn)行personal projects的開(kāi)發(fā)皮获,每周對(duì)nose與nose2的實(shí)際開(kāi)發(fā)時(shí)間大概只有30分鐘,在這種情況下纹冤,nose與nose2都將很難再有大的改版與修正洒宝。

Green

不同與nose/nose2,green是單純?yōu)榱藦?qiáng)化unittest中test runner功能而出現(xiàn)的工具萌京。green所提供的只有一個(gè)功能強(qiáng)大雁歌、使用方便、測(cè)試報(bào)告美觀的test runner知残。如果你的項(xiàng)目中的測(cè)試都是以傳統(tǒng)unittest module撰寫而成的話靠瞎,green會(huì)是一個(gè)很好的test runner選擇。

使用green執(zhí)行測(cè)試:

pip install green
cd path/to/project
green

Doctest

Doctest的標(biāo)準(zhǔn)文檔在這里:

  1. Python2
  2. Python3

Doctest看起來(lái)像是在交互式運(yùn)行環(huán)境中的輸出,事實(shí)上也確實(shí)如此 :)

def square(x):
    """Squares x.

    >>> square(2)
    4
    >>> square(-2)
    4
    """

    return x * x

if __name__ == '__main__':
    import doctest
    doctest.testmod()

Doctest的作用是作為函數(shù)/類/模塊等單元的解釋和表述性文檔较坛。所以它們有如下特點(diǎn):

  1. 只有期望對(duì)外公開(kāi)的單元會(huì)提供doctest
  2. 這些doctest通常不是很細(xì)致

編寫doctest測(cè)試基本不需要學(xué)習(xí)新技能點(diǎn)印蔗,在交互式環(huán)境里運(yùn)行一下,然后把輸出結(jié)果檢查一下貼過(guò)來(lái)就可以了丑勤。

doctest還有一些高級(jí)用法华嘹,但基本上用不到,用到的時(shí)候再去查標(biāo)準(zhǔn)文檔好了法竞。 :)

Mox

Mox是Java EasyMock框架在Python中的實(shí)現(xiàn)耙厚。它一個(gè)過(guò)時(shí)的,很像mock的類庫(kù)岔霸。從現(xiàn)在開(kāi)始薛躬,你應(yīng)該放棄學(xué)習(xí)Mox,在任何情況下都用Mock就對(duì)了呆细。

參考 Mox的官方文檔

Mox is a mock object framework for Python based on the Java mock object framework EasyMock.
New uses of this library are discouraged.
People are encouraged to use https://pypi.python.org/pypi/mock instead which matches the unittest.mock library available in Python 3.

Mox3 是一個(gè)非官方的類庫(kù)型宝,是mox的Python3兼容版本

Mox3 is an unofficial port of the Google mox framework (http://code.google.com/p/pymox/) to Python 3. 
It was meant to be as compatible with mox as possible, but small enhancements have been made. 
The library was tested on Python version 3.2, 2.7 and 2.6.
Use at your own risk ;)

其它

  • tox(官方文檔): 一個(gè)自動(dòng)化測(cè)試框架

    • checking your package installs correctly with different Python versions and interpreters
    • running your tests in each of the environments, configuring your test tool of choice
    • acting as a frontend to Continuous Integration servers, greatly reducing boilerplate and merging CI and shell-based testing.

    Basic example:

      # content of: tox.ini , put in same dir as setup.py
      [tox]
      envlist = py26,py27
      [testenv]
      deps=pytest       # install pytest in the venvs
      commands=py.test  # or 'nosetests' or ...
    

    You can also try generating a tox.ini file automatically, by running tox-quickstart and then answering a few simple questions.
    To sdist-package, install and test your project against Python2.6 and Python2.7, just type: tox

  • testr(官方文檔): 是一個(gè)test runner。

  • Django的Unittest(官方文檔)

    • 官方文檔推薦用Unittest:The preferred way to write tests in Django is using the unittest module built in to the Python standard library.

    • django.test.TestCase繼承了unittest.TestCase

      Here is an example which subclasses from django.test.TestCase, which is a subclass of unittest.TestCase that runs each test inside a transaction to provide isolation:

        from django.test import TestCase
        from myapp.models import Animal
        
        class AnimalTestCase(TestCase):
            def setUp(self):
                Animal.objects.create(name="lion", sound="roar")
                Animal.objects.create(name="cat", sound="meow")
        
            def test_animals_can_speak(self):
                """Animals that can speak are correctly identified"""
                lion = Animal.objects.get(name="lion")
                cat = Animal.objects.get(name="cat")
                self.assertEqual(lion.speak(), 'The lion says "roar"')
                self.assertEqual(cat.speak(), 'The cat says "meow"')
      
  • Flask的Unittest

    • 官方文檔中介紹:Flask provides a way to test your application by exposing the Werkzeug test Client and handling the context locals for you. You can then use that with your favourite testing solution. In this documentation we will use the unittest package that comes pre-installed with Python.

      app.test_client()

        app = flask.Flask(__name__)
        
        with app.test_client() as c:
            rv = c.get('/?tequila=42')
            assert request.args['tequila'] == '42'
      

      Unittest with Flask

        class FlaskrTestCase(unittest.TestCase):
        
            def setUp(self):
                self.db_fd, flaskr.app.config['DATABASE'] = tempfile.mkstemp()
                self.app = flaskr.app.test_client()
                flaskr.init_db()
        
            def tearDown(self):
                os.close(self.db_fd)
                os.unlink(flaskr.app.config['DATABASE'])
        
            def test_empty_db(self):
                rv = self.app.get('/')
                assert b'No entries here so far' in rv.data
      
    • 擴(kuò)展:Flask-Testing

        @app.route("/ajax/")
        def some_json():
            return jsonify(success=True)
        
        class TestViews(TestCase):
            def test_some_json(self):
                response = self.client.get("/ajax/")
                self.assertEquals(response.json, dict(success=True))
      

建議和總結(jié)

  • 在項(xiàng)目中盡量不要mix多種功能類似的框架絮爷。

    你可以選unittest + green趴酣,或者nose/nose2(依使用Python版本和項(xiàng)目的歷史遺留而定) ,或者pytest坑夯,但是盡量不要混合使用岖寞。

  • 關(guān)于Unittest

    • 如果沒(méi)有特別的原因,新項(xiàng)目應(yīng)該用unittest柜蜈。
    • Unittest中要用Assert仗谆,不要用FailUnless
    • Django和Flask中都應(yīng)該用unittest框架,他們也都提供了一個(gè)unittest.TestCase的子類以便于做與WebServer想關(guān)的測(cè)試
  • 關(guān)于Mock

    • 如果要Mock一個(gè)對(duì)象淑履,用MagicMock
    • 如果要在函數(shù)或者with-statment中Mock一個(gè)對(duì)象隶垮,用patch
    • 如果要在函數(shù)或者with-statement中Mock一個(gè)對(duì)象的屬性,用patch.object
    • 如果要在函數(shù)或者with-statement中Mock一個(gè)字典(增加或重建一個(gè)鍵值對(duì))秘噪,用patch.dict
    • 如果要在函數(shù)或者with-statement中一次Patch多個(gè)Mock對(duì)象狸吞,用patch.multiple
    • 如果希望Mock對(duì)象擁有和原對(duì)象相同的字段和方法(對(duì)于方法對(duì)象,則擁有相同的簽名)缆娃,用create_autospec捷绒。
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市贯要,隨后出現(xiàn)的幾起案子暖侨,更是在濱河造成了極大的恐慌,老刑警劉巖崇渗,帶你破解...
    沈念sama閱讀 206,968評(píng)論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件字逗,死亡現(xiàn)場(chǎng)離奇詭異京郑,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)葫掉,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,601評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門些举,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人俭厚,你說(shuō)我怎么就攤上這事户魏。” “怎么了挪挤?”我有些...
    開(kāi)封第一講書人閱讀 153,220評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵叼丑,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我扛门,道長(zhǎng)鸠信,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書人閱讀 55,416評(píng)論 1 279
  • 正文 為了忘掉前任论寨,我火速辦了婚禮星立,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘葬凳。我一直安慰自己绰垂,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,425評(píng)論 5 374
  • 文/花漫 我一把揭開(kāi)白布沮明。 她就那樣靜靜地躺著辕坝,像睡著了一般窍奋。 火紅的嫁衣襯著肌膚如雪荐健。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書人閱讀 49,144評(píng)論 1 285
  • 那天琳袄,我揣著相機(jī)與錄音江场,去河邊找鬼。 笑死窖逗,一個(gè)胖子當(dāng)著我的面吹牛址否,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播碎紊,決...
    沈念sama閱讀 38,432評(píng)論 3 401
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼佑附,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了仗考?” 一聲冷哼從身側(cè)響起音同,我...
    開(kāi)封第一講書人閱讀 37,088評(píng)論 0 261
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎秃嗜,沒(méi)想到半個(gè)月后权均,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體顿膨,經(jīng)...
    沈念sama閱讀 43,586評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,028評(píng)論 2 325
  • 正文 我和宋清朗相戀三年叽赊,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了恋沃。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,137評(píng)論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡必指,死狀恐怖囊咏,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情塔橡,我是刑警寧澤匆笤,帶...
    沈念sama閱讀 33,783評(píng)論 4 324
  • 正文 年R本政府宣布,位于F島的核電站谱邪,受9級(jí)特大地震影響炮捧,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜惦银,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,343評(píng)論 3 307
  • 文/蒙蒙 一咆课、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧扯俱,春花似錦书蚪、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書人閱讀 30,333評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至读存,卻和暖如春为流,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背让簿。 一陣腳步聲響...
    開(kāi)封第一講書人閱讀 31,559評(píng)論 1 262
  • 我被黑心中介騙來(lái)泰國(guó)打工敬察, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人尔当。 一個(gè)月前我還...
    沈念sama閱讀 45,595評(píng)論 2 355
  • 正文 我出身青樓莲祸,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親椭迎。 傳聞我的和親對(duì)象是個(gè)殘疾皇子锐帜,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,901評(píng)論 2 345

推薦閱讀更多精彩內(nèi)容