unittest_about.md

本文深入解析unittest框架,涵盖基本使用、重要模块如loader、runner、result、suite和case,以及装饰器、断言方法和测试结果收集。通过实例展示unittest在自动化测试中的应用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

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': []}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值