天天看點

unittest_about.mdunittest相關

unittest相關

unittest是一個标準庫,無需我們額外安裝。unittest是一個優秀的單元測試架構,其适用性和擴充性也都比較好。我們的很多自動化架構都是基于unittest而擴充出來的。本次講解基礎使用,以舉例進行延伸。

先說一下unittest的幾個重要子產品

  1. loader
  2. runner
  3. result
  4. suite
  5. case

其他的基本上都是服務于以上5個子產品。我們開始一個個講,但是未必按照以上順序。

case > TestCase

case是一個用例,是一個最小執行單元,也可以認為是一個case類下的一個函數。

舉個簡單例子:為示範效果,建立為普通python檔案

#gm_test.py
import unittest

class GmTestCase(unittest.TestCase):
    def gmfunc(self):
        self.assertEqual(True,False)

if __name__ == '__main__':
    gmtestcase = GmTestCase('gmfunc')
    gmtestcase.run()
           

使用python3 gm_test.py 執行,這樣就完成了最簡單的使用,當然我們的真實場景遠比這個要複雜。gmfunc的邏輯也會複雜些。

我們看一下這個腳本,首先我們導入了unittest子產品->建立了一個繼承了TestCase的類->定義了一個執行個體方法->在__main__建立了一個最小執行單元的執行個體gmtestcase->調用了執行個體方法run。run方法裡會先調用setUp,再執行gmfunc,最後執行tearDown。

其實這便是單元case的實作,當然我們一個TestCase子類裡面可能不止一個方法,甚至不止一個TestCase子類,比如下面:

import unittest

class GmTestCase(unittest.TestCase):
    def gmfunc(self):
        self.assertEqual(True,False)
    def gmfunc_01(self):
        self.assertEqual(True,False)

class GmTestCase_01(unittest.TestCase):
    def gmfunc(self):
        self.assertEqual(True,False)
    def gmfunc_01(self):
        self.assertEqual(True,False)
        
if __name__ == '__main__':
    gmtestcase = GmTestCase('gmfunc')
    gmtestcase.run()
           

我們可以指定任意一個方法進行執行,比如我要執行GmTestCase_01的gmfunc_01,那麼隻需要如下:

if __name__ == '__main__':
    gmtestcase = GmTestCase_01('gmfunc_01')
    gmtestcase.run()
           

=assert=

我們再看下,裡面的assert方法,Testcase本身提供了很多的assert執行個體方法,根據需要取用。

['assertAlmostEqual', 'assertAlmostEquals', 'assertCountEqual', 'assertDictContainsSubset', 'assertDictEqual', 'assertEqual', 'assertEquals', 'assertFalse', 'assertGreater', 'assertGreaterEqual', 'assertIn', 'assertIs', 'assertIsInstance', 'assertIsNone', 'assertIsNot', 'assertIsNotNone', 'assertLess', 'assertLessEqual', 'assertListEqual', 'assertLogs', 'assertMultiLineEqual', 'assertNotAlmostEqual', 'assertNotAlmostEquals', 'assertNotEqual', 'assertNotEquals', 'assertNotIn', 'assertNotIsInstance', 'assertNotRegex', 'assertNotRegexpMatches', 'assertRaises', 'assertRaisesRegex', 'assertRaisesRegexp', 'assertRegex', 'assertRegexpMatches', 'assertSequenceEqual', 'assertSetEqual', 'assertTrue', 'assertTupleEqual', 'assertWarns', 'assertWarnsRegex']

那你可能想說,執行結果呢?那麼下面介紹result子產品~

result > TestResult

我們進行測試後的結果如何收集,unittest貼心的設計了result子產品,那麼這個result子產品怎麼使用呢?結合case使用,我們看一下下面的例子:

import unittest

class GmTestCase(unittest.TestCase):
    def gmfunc(self):
        self.assertEqual(True,False)

if __name__ == '__main__':
    result= unittest.TestResult()
    gmtestcase = GmTestCase('gmfunc')
    gmtestcase.run(result)
    print(result.__dict__)
           
小竅門:pprint可以直接根據列印内容類型優美顯示

result是一個TestResult的執行個體,有興趣的同學可以去看看這個TestResult類是怎麼設計的,我們看一下列印的内容吧。

{'_mirrorOutput': False,
 '_original_stderr': <_io.TextIOWrapper name='<stderr>' mode='w' encoding='UTF-8'>,
 '_original_stdout': <_io.TextIOWrapper name='<stdout>' mode='w' encoding='UTF-8'>,
 '_stderr_buffer': None,
 '_stdout_buffer': None,
 'buffer': False,
 'errors': [],
 'expectedFailures': [],
 'failfast': False,
 'failures': [(<__main__.GmTestCase testMethod=gmfunc>,
               'Traceback (most recent call last):\n'
               '  File "gm_test.py", line 6, in gmfunc\n'
               '    self.assertEqual(True,False)\n'
               'AssertionError: True != False\n')],
 'shouldStop': False,
 'skipped': [],
 'tb_locals': False,
 'testsRun': 1,
 'unexpectedSuccesses': []}
           

可能有同學看過統計錯誤和正确數量的參數,但是TestResult本身就是這些參數,當然我們可以根據自己的需要封裝。

是以,result是作為case單元run方法的一個參數傳進去的,result根據執行的過程不斷更新自己的内容。

TestResult本身或者說三方報表子產品是很重要的(直接影響報表)

我們知道,result是傳給TestCase執行個體的,當然直接使用case執行個體的run不傳result,unittest本身也會替咱們造一個result,這個result不斷的根據case執行個體的進行更新,我們看一看result幾個重要的方法以及何時會用~

STEP1
執行startTestRun 方法,根據需要決定是否執行
STEP2
判斷case是否需要執行不執行(後面會講裝飾器skip,這個裝飾器決定),如果不需要執行,調用_addSkip方法(這個方法會調用addSkip方法,這個方法TestResult是沒有的,放開了這個方法給了繼承TestResult的子類,如果子類也沒有定義,那麼調用addSuccess方法),這裡需要注意的是,所有的我說的方法都是result執行個體的調用
STEP3
  1. case執行個體執行,執行順序為setUP、case的method本身,tearDown,注意:如果setUp失敗了,不會執行method和tearDown
  2. 根據setUp、method、tearDown的執行結果,如果執行正确,調用addSuccess;若報錯且方法使用了裝飾器expecting_failure,調用_addExpectedFailure;如果是assert報錯,調用addFailure;如果是其他報錯,調用addError;至于調用的這些方法又做了哪些事有興趣的可以去看下,無非就是增加修改result裡面的字段。
STEP4
執行stopTestRun 方法,根據需要決定是否執行

PS:那麼我不想一個個執行run方法,還是多次執行個體化好麻煩,怎麼辦,我們接下來看一下suite吧

suite > TestSuite

suite就是對case做了一個集合,當執行suite的run方法時,就是對集合内所有的case進行了run方法(串行),這樣可以有效控制哪些case被執行。看下面:

import unittest
from pprint import pprint

class GmTestCase(unittest.TestCase):
    def gmfunc(self):
        self.assertEqual(True, False)

    def gmfunc_01(self):
        self.assertEqual(True, False)

class GmTestCase_01(unittest.TestCase):
    def gmfunc(self):
        self.assertEqual(True, False)

    def gmfunc_01(self):
        self.assertEqual(True, False)

if __name__ == '__main__':
    result = unittest.TestResult()
    suites = unittest.TestSuite()
    suites.addTests([GmTestCase('gmfunc'),GmTestCase_01('gmfunc_01')])
    suites.run(result)
    pprint(result.__dict__)
           

我們顯示建立了一個TestSuite執行個體,然後将GmTestCase的gmfunc和GmTestCase_01的gmfunc_01的case執行個體通過addTests方法添加進來,最後執行了run方法。

注意:case的run方法,result參數不是必傳的,但是suite的run方法是必須傳入result執行個體的。

suite本身的原理也不難了解,就如上所說,他是各個case的集合,是一個套件管理器的作用。但是有一點需要注意,在suite執行run方法時,會執行下case所在類的setUpClass方法(若有)。

另一方面suite本身集合是可以多層嵌套的,比如上面__main__的代碼,改為下面也是可以的,執行結果不會發生變化

if __name__ == '__main__':
    result = unittest.TestResult()
    suites = unittest.TestSuite()
    suites.addTests([GmTestCase('gmfunc'),GmTestCase_01('gmfunc_01')])
    suites_new = unittest.TestSuite()
    suites_new.addTests(suites)
    suites_new.run(result)
    pprint(result.__dict__)
           
小竅門:當然我們不禁止使用addTests時 tests的順序,但是一般都會同一個類的方法是連續的,不然setUpClass的方法就會失去意義,比如suites.addTests([GmTestCase('gmfunc'),GmTestCase_01('gmfunc_01'),GmTestCase('gmfunc_01'),])就不好,GmTestCase的兩個case中間有一個GmTestCase_01的case。

那麼問題又來了,我覺得suite也好麻煩,還得addTests什麼的,我就想敲一個指令把所有的需要執行的case全部自動生成好加到suite裡面來。如此貼心的unittest自然也提供了這種方法,那就是loader~

loader > TestLoader

loader的原理就是:

  1. 在指定的一個檔案夾下找到所有的滿足一定條件的py檔案(支援多層檔案夾),預設是test*.py,
  2. 将py檔案當做子產品導入sys.modules中,提取出每個py檔案裡面繼承了TestCase的類,生成一個suite
  3. 提取出這個TestCase類的所有滿足條件的方法執行個體化為case後進入suite中,預設是test開頭(這個不能配置),是以注意一定要遵守,那麼我之前舉例中的執行個體方法名用這個loader實際上是統計不到的。
  4. 循環以上操作,将suite再次加個到一個大的suite中。
高能注意:如果想自定pattern,注意比對是re.match方法

loader也就是TestLoader有一些通過名字,py檔案名獲得case的方法,但是我們都不用關注,我們隻需要知道一個方法discover

def discover(self, start_dir, pattern='test*.py', top_level_dir=None):
        """Find and return all test modules from the specified start
        directory, recursing into subdirectories to find them and return all
        tests found within them. Only test files that match the pattern will
        be loaded. (Using shell style pattern matching.)

        All test modules must be importable from the top level of the project.
        If the start directory is not the top level directory then the top
        level directory must be specified separately.

        If a test package name (directory with '__init__.py') matches the
        pattern then the package will be checked for a 'load_tests' function. If
        this exists then it will be called with (loader, tests, pattern) unless
        the package has already had load_tests called from the same discovery
        invocation, in which case the package module object is not scanned for
        tests - this ensures that when a package uses discover to further
        discover child tests that infinite recursion does not happen.

        If load_tests exists then discovery does *not* recurse into the package,
        load_tests is responsible for loading all tests in the package.

        The pattern is deliberately not stored as a loader attribute so that
        packages can continue discovery themselves. top_level_dir is stored so
        load_tests does not need to pass this argument in to loader.discover().

        Paths are sorted before being imported to ensure reproducible execution
        order even on filesystems with non-alphabetical ordering like ext3/4.
        """
           
start_dir: 就是要收集case的檔案夾
pattern: 收集哪些py檔案内case的标準
top_level_dir: 這個之前用過,我簡單看了下,就是相當于把這個檔案夾進行子產品化,友善對start_dir使用__import__,這種情況一般時執行start_dir不在 主路徑下,比如我想僅僅跑testCase檔案夾下的testSubcase檔案夾下的testSub檔案夾下的腳本,那麼就需要使用_unit = unittest.defaultTestLoader.discover('testsub', pattern= '*_test.py', top_level_dir='testCase/testsubcase')
傳回值是一個suite,可以直接執行suite(result)了

講到這裡了,基本上就講完了,真的講完了·····好吧,其實就剩runner沒有講了,那簡單介紹下!

runner > TestRunner

為什麼之前說基本上講完了呢,因為runner這貨可以說就完成了很小的一個事情,甚至很多地方都用不到,就是在使用python -m unitest xxx.py時執行過程中,建立了一個result并傳給了suite。而我們三方的報表子產品根本沒借助這個runner,而是自主的去完成了result傳給suite。

unittest幾個大子產品就算講完了,後期就是講架構的内容了,如何産生報告,如何維護用例,如何架構分層等等了,此外再補充3點吧。

關于setUp、setUpClass和tearDown、tearDownClass

setUp和tearDown是case的前置和後置方法,屬于執行個體方法

setUpClass和tearDownClass是suite的前置和後置方法,屬于類方法

這些方法都可以同時存在,并不沖突,就是說,我在TestCase子類裡面,既可以寫一個 執行個體方法setUp同時也可以寫一個類方法setUpClass,當進入到新的suite時,會執行一次setUpClass,執行每一個case時,會執行一次setUp。tearDown和tearDownClass也是同樣的原理。

python -m unittest

後面可以跟一個py檔案,可以跟一個TestCase類,也可以是一個方法 比如:

python -m unittest  gm_test.py
python -m unittest  gm_test.GmTestCase
python -m unittest  gm_test.GmTestCase.test_gmfunc_04
           

unitest提供了一種單個執行某個腳本的方法,這個方法會自動将xxx.py裡面所有滿足條件(條件同loader,因為會調用loader的方法)的case封裝好進行執行。當我們在pycharm建立py檔案時指定了是unittest,那麼右鍵執行時,其實也是執行了該方法,就不會跑到__main__代碼中了。

裝飾器

有同學知道pytest有很多裝飾器,我們的unittest也是有的···恩,是有的,但是很少,都是針對TestCase或者裡面方法的裝飾,舉個例子吧:

目前看隻有三個裝飾器 skip ,skipIf ,skipUnless 都是skip的,還有一個expectedFailure,是報錯也不計數的,其實使用報表子產品的話,這些計數是報表子產品本身的功能,不會收這幾個裝飾器影響。
import unittest
from pprint import pprint
from datetime import datetime


class GmTestCase(unittest.TestCase):
    @classmethod
    def setUpClass(cls):
        print('9' * 99)

    @unittest.skip('我懶得執行這個方法')
    def gmfunc(self):
        self.assertEqual(True, False)

    @unittest.skipIf(datetime.today().day > 15, '今天是月下旬,我肯定不執行這個哦~')
    def gmfunc_01(self):
        self.assertEqual(True, False)

    @unittest.skipUnless(datetime.today().day == 30 and datetime.today().month == 2, '除非今天是2.30,不然我不執行這個!')
    def gmfunc_02(self):
        self.assertEqual(True, False)

    @unittest.expectedFailure
    def gmfunc_03(self):
        self.assertEqual(True, False)

    def gmfunc_04(self):
        '''隻有我才是正常的'''
        self.assertEqual(True, False)


@unittest.skip('我不執行這個類哦')
class GmTestCase_01(unittest.TestCase):

    def gmfunc(self):
        self.assertEqual(True, False)


if __name__ == '__main__':
    result = unittest.TestResult()
    suites = unittest.TestSuite()
    suites.addTests([GmTestCase('gmfunc'),
                     GmTestCase('gmfunc_01'),
                     GmTestCase('gmfunc_02'),
                     GmTestCase('gmfunc_03'),
                     GmTestCase('gmfunc_04'),
                     GmTestCase_01('gmfunc')])
    suites_new = unittest.TestSuite()
    suites_new.addTests(suites)
    suites_new.run(result)
    pprint(result.__dict__)
           

看一下結果:

{'_mirrorOutput': False,
 '_moduleSetUpFailed': False,
 '_original_stderr': <_io.TextIOWrapper name='<stderr>' mode='w' encoding='UTF-8'>,
 '_original_stdout': <_io.TextIOWrapper name='<stdout>' mode='w' encoding='UTF-8'>,
 '_previousTestClass': <class '__main__.GmTestCase_01'>,
 '_stderr_buffer': None,
 '_stdout_buffer': None,
 '_testRunEntered': False,
 'buffer': False,
 'errors': [],
 'expectedFailures': [(<__main__.GmTestCase testMethod=gmfunc_03>,
                       'Traceback (most recent call last):\n'
                       '  File "gm_test.py", line 25, in gmfunc_03\n'
                       '    self.assertEqual(True, False)\n'
                       'AssertionError: True != False\n')],
 'failfast': False,
 'failures': [(<__main__.GmTestCase testMethod=gmfunc_04>,
               'Traceback (most recent call last):\n'
               '  File "gm_test.py", line 29, in gmfunc_04\n'
               '    self.assertEqual(True, False)\n'
               'AssertionError: True != False\n')],
 'shouldStop': False,
 'skipped': [(<__main__.GmTestCase testMethod=gmfunc>, '我懶得執行這個方法'),
             (<__main__.GmTestCase testMethod=gmfunc_01>, '今天是月下旬,我肯定不執行這個哦~'),
             (<__main__.GmTestCase testMethod=gmfunc_02>,
              '除非今天是2.30,不然我不執行這個!'),
             (<__main__.GmTestCase_01 testMethod=gmfunc>, '我不執行這個類哦')],
 'tb_locals': False,
 'testsRun': 6,
 'unexpectedSuccesses': []}