关于unittest单元测试框架

简介

unittest官方文档
翻译得不错的文档
详细还是看官方文档,我这里只是对官方文档中的一些笔记简要。
unittest是python标准库里的工具,不需要额外安装,它是一个单元测试框架与pytest的作用是一样的,两者是可以互换。

单元测试框架的作用

  1. 发现测试用例
  2. 执行测试用例
  3. 判断测试结果
  4. 生成测试报告

unittest五个重要组件

  • TestCase测试用例:最小单元,业务逻辑
  • TestSuite测试套件:一组测试用例的集合,或者测试套件的集合。
  • TestFixtrue测试夹具:执行测试用例之前和之后的操作
    • unittest:
      • setUp/tearDown 在测试用例之前/后执行,每个用例都会执行
      • setUpClass/tearDownClass 在测试类之前/后执行,同一个类,多个用例只执行一次,注意类要加装饰器
      • setUpModule/tearDownModule 在测试模块之前和之后执行
    • pytest:
      • setup/teardown 全小写
      • setup_class/teardown_class
      • setup_module/teardown_class

TestLoader测试加载器:加载测试用例
TestRunner测试运行器:运行指定的测试用例。

运行方式

# test_module1.py
import unittest
class TestClass(unittest.TestCase):
    def setUp(self):
        print('setUp')

    def tearDown(self):
        print('tearDown')

    def test_method(self):
        print('test_1')
    
    @unittest.skip("skip test_2")
    def test_2(self):
        print('test_2')

def suite():
    suite=unittest.TestSuite()
    suite.addTest(TestClass('test_method'))
    suite.addTest(TestClass('test_2'))
    return suite



if __name__ == '__main__':
    runner=unittest.TextTestRunner()
    runner.run(suite())
  • 命令行
    python -m unittest test_module1 test_module2
    python -m unittest test_module.TestClass
    python -m unittest test_module.TestClass.test_method
    python -m unittest -k test_module1.Test*
    -k 使用通配符
    为什么用命令行?因为当测试用例很多的时候,我们可以通过测试平台,指定要运行哪个用例,而不需要去修改代码。
  • 通过测试套件来运行
    • AddTest
# 创建一个测试套件
suite=unittest.TestSuite()
suite.addTest(TestClass('test_method'))
unittest.main(defaultTest='suite')
  • AddTests
suite=unittest.TestSuite()
suite.addTests(TestClass('test_method'),TestClass('test_2')
unittest.main(defaultTest='suite')
  • 加载一个目录下的测试用例
if __name__ == '__main__':
    suite = unittest.defaultTestLoader.discover('./__path__', pattern = '*.py')
    unittest.main(defaultTest='suite')

说说unittest.main()

main()

def __init__(self, module='__main__', defaultTest=None, argv=None,
                    testRunner=None, testLoader=loader.defaultTestLoader,
                    exit=True, verbosity=1, failfast=None, catchbreak=None,
                    buffer=None, warnings=None, *, tb_locals=False):

代码了后一行,main = TestProgram,也就是main是TestProgram类的一个实例,当这个时候就会实例化地执行init函数。
main()里有很多参数
module='main' : 要放在if name == 'main' 是入口函数。
defaultTest=None:默认为全部,可以指定运行某一个。
argv: 可以传递参数进去
testRunner: 测试运行器
testLoader: 测试加载器,使用的是默认的测试使用加载器
exit=True: 是否在测试程序完成之后关闭程序。
verbosity=1:显示信息的详细程度

  • <=0 只显示用例 的总数和全局的执行结果
  • 1 默认值,显示用例总数和全局结果,并且对每个用例的结果有个标。
    .成功
    F失败
    E错误
    S用例跳过
  • >=2 显示用例总数和全局结果,并输出每个用例的详解的结果
    failfast=None:是否在测试失败时终止测试
    catchbreak=None:
    buffer=None:
    warnings=None:
    tb_locals=False:

init函数最后self.runTests()运行了这个函数。跳到246行。

if self.testRunner is None:
            self.testRunner = runner.TextTestRunner
# 和我们平时写的
if __name__ == '__main__':
    runner=unittest.TextTestRunner()
    runner.run(suite())

回顾一下unittest单元测试框架的五个重要部分

  • TestCase测试用例:最小单元,业务逻辑
  • TestSuite测试套件:一组测试用例的集合,或者测试套件的集合。
    测试用例或套件都在此就绪
# self.testNames 加载了我们所需要的用例并赋给了self.test,
def createTests(self, from_discovery=False, Loader=None):
        if self.testNamePatterns:
            self.testLoader.testNamePatterns = self.testNamePatterns
        if from_discovery:
            loader = self.testLoader if Loader is None else Loader()
            self.test = loader.discover(self.start, self.pattern, self.top)
        elif self.testNames is None:
            self.test = self.testLoader.loadTestsFromModule(self.module)
        else:
            self.test = self.testLoader.loadTestsFromNames(self.testNames,
                                                           self.module)
# 250行
self.testRunner = runner.TextTestRunner
# 271行
self.result = testRunner.run(self.test)

  • TestFixtrue测试夹具:执行测试用例之前和之后的操作
  • TestLoader测试加载器:加载测试用例
def __init__(self, module='__main__', defaultTest=None, argv=None,
                    testRunner=None, testLoader=loader.defaultTestLoader,
                    exit=True, verbosity=1, failfast=None, catchbreak=None,
                    buffer=None, warnings=None, *, tb_locals=False):
  • TestRunner测试运行器:运行指定的测试用例。
if self.testRunner is None:
            self.testRunner = runner.TextTestRunner

总结:
先从main函数开始,它是一个类,而这个类通过TestProgram实例化,在实例化时就会运行init初始化函数,对每个参数进行初始化,关键的是五个部分(用例、测试运行器,测试加载器,实际最常见的是三个),通过我们的模块和测试名称来加载出来。所以从原理来说,只是执行了两句代码,其它都是判断。

# test_case/a.py
import unittest
class TestAll(unittest.TestCase):
    @classmethod
    def setUpClass(cls) -> None:
        print('setUpClass')
    # 测试用例
    def test_quickly_process(self):
        print('test_quickly_process')   
    @classmethod
    def tearDownClass(cls) -> None:
        print('tearDownClass')
if __name__ == '__main__':
    unittest.main()

# test_case/b.py
import unittest

class TestModel(unittest.TestCase):
    @classmethod
    def setUpClass(cls) -> None:
        print('setUpClass')

    # 测试用例
    def test_ceiling_model(self):
        print('test_ceiling_model')
    
    def test_cermic_model(self):
        print('test_cermic_model')
    
    def test_door_model(self):
        print('test_door_model')
    
    def test_products_model(self):
        print('test_products_model')   

    @classmethod
    def tearDownClass(cls) -> None:
        print('tearDownClass')

if __name__ == '__main__':
    unittest.main()


# test.py
import unittest

if __name__ == "__main__":
    # 就这两句
    # 测试加载器,加载我们用例里的所有*.py文件
    suite = unittest.defaultTestLoader.discover('./test_case', pattern='*.py')
    # 测试运行器
    unittest.TextTestRunner().run(suite)
    # 为什么main()会报错?因为main中deaultTest为空时实际上只会执行当前文件的用例,但是我们的文件现时是空的,没有任何用例所以报错,如果指定了deaultTest='suite'后,他就知道从这个加载器里找这些测试用例了。
    # unitest.main(deaultTest='suite')

运行结果


image.png

测试夹具

测试夹具(测试脚手架、固件、钩子函数,前后置等)TextFixtrue

  • TestFixtrue测试夹具:执行测试用例之前和之后的操作
    • unittest:
      • setUp/tearDown 在测试用例之前/后执行,每个用例都会执行
      • setUpClass/tearDownClass 在测试类之前/后执行,同一个类,多个用例只执行一次,注意类要加装饰器
      • setUpModule/tearDownModule 在测试模块之前和之后执行
    • pytest:
      • setup/teardown 全小写
      • setup_class/teardown_class
      • setup_module/teardown_class
# ./test_case/b.py
import unittest

class TestModel(unittest.TestCase):
    # 类的装饰器不能缺,缺了就报错。测试类之前的准备工作:连接数据库,创建日志对象等
    @classmethod
    def setUpClass(cls) -> None:
        print('setUpClass,每个测试类运行前运行')
    
    @classmethod
    def tearDownClass(cls) -> None:
        print('tearDownClass,每个测试类运行后运行')
     
    # 测试用例 前的准备工作:可能是打开浏览器,加载网页,登陆、选择好场景等等。
    def setUp(self) -> None:
        print('setUp每个用例启动前运行')    
  
    def tearDown(self) -> None:
        print('setUp每个用例结束后前运行')

    # 测试用例
    def test_ceiling_model(self):
        print('test_ceiling_model')
    
    def test_cermic_model(self):
        print('test_cermic_model')
    
    def test_door_model(self):
        print('test_door_model')
    
    def test_products_model(self):
        print('test_products_model')   

if __name__ == '__main__':
    unittest.main()
image.png

夹具的封装

那么如何我们多个用例存在相同的setUp/tearDown setUpClass/tearDownClass那怎么办?我们可以进行夹具封装

# common/unit.py
import unittest
class YfUnit(unittest.TestCase):
    @classmethod
    def setUpClass(cls) -> None:
        print('setUpClass,每个测试类运行前运行')
    
    @classmethod
    def tearDownClass(cls) -> None:
        print('tearDownClass,每个测试类运行后运行')
    
    def setUp(self) -> None:
        print('setUp每个用例启动前运行')
    
    def tearDown(self) -> None:
        print('setUp每个用例结束后前运行')

那么两个test_case可以继承这个YfUnit,修改为,仅用于写用例

#test_case/a.py
from common.unit import YfUnit
class TestModel(YfUnit):
     # 测试用例
    def test_ceiling_model(self):
        print('test_ceiling_model')    
    def test_cermic_model(self):
        print('test_cermic_model')    
    def test_door_model(self):
        print('test_door_model')    
    def test_products_model(self):
        print('test_products_model') 

# test_case/b.py
from common.unit import YfUnit


class TestAll(YfUnit):
    # 测试用例

    def test_quickly_process(self):
        print('test_quickly_process')
# all.py

import unittest

if __name__ == "__main__":
    # 测试加载器,加载我们用例里的所有*.py文件
    suite = unittest.defaultTestLoader.discover('test_case', pattern='*.py')
    # 测试运行器
    unittest.TextTestRunner().run(suite)

关于用例的添加

Test Suite

通过addTest或addTests把要测试用例添加到套件里
addTest 单独添加测试用例,内容为:类名(“方法名”);
Test2是要测试的类名,test_one是要执行的测试方法
执行其余的方法直接依照添加
suite.addTest(Test2("test_two"))
suite.addTest(Test2("test_one"))

addTests 是将需要执行的测试用例放到一个list后,再进行add,addTests 格式为:addTests(用例list名称);
tests = [Test2("test_two"), Test2("test_one")]
suite.addTests(tests)

这种方式控制粒度到方法,但是如何一个测试类里,有好多个方法呢?

Test Loder

TestLoadder经常用来从类和模块中提取创建测试套件。一般情况下TestLoader不需要实例化,unittest内部提供了一个实例可以当做unittest.defaultTestLoader使用。无论使用子类还是实例,都允许自定义一些配置属性。
loadTestsFrom*()方法从各个地方寻找testcase,创建实例,然后addTestSuite,再返回一个TestSuite实例
defaultTestLoader()TestLoader()功能差不多,复用原有实例

  • unittest.TestLoader().loadTestsFromTestCase(测试类名(方法名))
    从TestCase派生类testCaseClass中加载所有测试用例并返回测试套件。
    getTestCaseNames()会从每一个方法中创建Test Case实例,这些方法默认都是test开头命名。如果getTestCaseNames()没有返回任何方法,但是runTest()方法被实现了,那么一个test case就会被创建以替代该方法。
def loadTestsFromTestCase(self, testCaseClass):
        """Return a suite of all test cases contained in testCaseClass"""
        if issubclass(testCaseClass, suite.TestSuite):
            raise TypeError("Test cases should not be derived from "
                            "TestSuite. Maybe you meant to derive from "
                            "TestCase?")
        testCaseNames = self.getTestCaseNames(testCaseClass)
        if not testCaseNames and hasattr(testCaseClass, 'runTest'):
            testCaseNames = ['runTest']
        loaded_suite = self.suiteClass(map(testCaseClass, testCaseNames))
        return loaded_suite
  • unittest.TestLoader().loadTestsFromModule(测试模块名)
    目测是python 3.5后不再兼容,可以使用loadTestsFromTestCase(测试类名)代替,也就是不填方法名
    从模块中加载所有测试用例,返回一个测试套件。该方法会从module中查找TestCase的派生类并为这些类里面的测试方法创建实例。
    注意:当使用TestCase派生类时可以共享test fixtures和一些辅助方法。该方法不支持基类中的测试方法。但是这么做是有好处的,比如当子类的fixtures不一样时。
    如果一个模块提供了load_tests方法,它会在加载测试时被调用。这样可以自定义模块的测试加载方式。这就是load_tests协议。pattern作为第三个参数被传递给load_tests。
# XXX After Python 3.5, remove backward compatibility hacks for
    # use_load_tests deprecation via *args and **kws.  See issue 16662.
    def loadTestsFromModule(self, module, *args, pattern=None, **kws):
        """Return a suite of all test cases contained in the given module"""
        # This method used to take an undocumented and unofficial
        # use_load_tests argument.  For backward compatibility, we still
        # accept the argument (which can also be the first position) but we
        # ignore it and issue a deprecation warning if it's present.
        if len(args) > 0 or 'use_load_tests' in kws:
            warnings.warn('use_load_tests is deprecated and ignored',
                          DeprecationWarning)
            kws.pop('use_load_tests', None)
        if len(args) > 1:
            # Complain about the number of arguments, but don't forget the
            # required `module` argument.
            complaint = len(args) + 1
            raise TypeError('loadTestsFromModule() takes 1 positional argument but {} were given'.format(complaint))
        if len(kws) != 0:
            # Since the keyword arguments are unsorted (see PEP 468), just
            # pick the alphabetically sorted first argument to complain about,
            # if multiple were given.  At least the error message will be
            # predictable.
            complaint = sorted(kws)[0]
            raise TypeError("loadTestsFromModule() got an unexpected keyword argument '{}'".format(complaint))
        tests = []
        for name in dir(module):
            obj = getattr(module, name)
            if isinstance(obj, type) and issubclass(obj, case.TestCase):
                tests.append(self.loadTestsFromTestCase(obj))

        load_tests = getattr(module, 'load_tests', None)
        tests = self.suiteClass(tests)
        if load_tests is not None:
            try:
                return load_tests(self, tests, pattern)
            except Exception as e:
                error_case, error_message = _make_failed_load_tests(
                    module.__name__, e, self.suiteClass)
                self.errors.append(error_message)
                return error_case
        return tests
  • unittest.TestLoader().loadTestsFromName(方法名,类名)
    从一个字符串中加载测试并返回测试套件。
    字符串说明符name是一个虚名(测试方法名的字符串格式),可以适用于模块、测试类、test case类中的测试方法、TestSuite实例或者一个返回TestCase或TestSuite的可调用的对象。并且这样的话,在一个测试用例类中的方法会被当做‘测试用例类中给的测试方法’而不是‘可调用对象’。
    比如,你有一个SampleTests模块,里面有一个TestCase的派生类SampleTestCase,类中有3个方法(test_one(),test_two(),test_three()),使用说明符“SampleTests.SampleTestCase”就会返回一个测试套件,里面包含所有3个测试方法。如果使用“SampleTests.SampleTestCase.test_two”就会返回一个测试套件,里面只包含test_two()测试方法。如果说明符中包含的模块或包没有事先导入,那么在使用时会被顺带导入。
def loadTestsFromName(self, name, module=None):
        """Return a suite of all test cases given a string specifier.

        The name may resolve either to a module, a test case class, a
        test method within a test case class, or a callable object which
        returns a TestCase or TestSuite instance.

        The method optionally resolves the names relative to a given module.
        """
        parts = name.split('.')
        error_case, error_message = None, None
        if module is None:
            parts_copy = parts[:]
            while parts_copy:
                try:
                    module_name = '.'.join(parts_copy)
                    module = __import__(module_name)
                    break
                except ImportError:
                    next_attribute = parts_copy.pop()
                    # Last error so we can give it to the user if needed.
                    error_case, error_message = _make_failed_import_test(
                        next_attribute, self.suiteClass)
                    if not parts_copy:
                        # Even the top level import failed: report that error.
                        self.errors.append(error_message)
                        return error_case
            parts = parts[1:]
        obj = module
        for part in parts:
            try:
                parent, obj = obj, getattr(obj, part)
            except AttributeError as e:
                # We can't traverse some part of the name.
                if (getattr(obj, '__path__', None) is not None
                    and error_case is not None):
                    # This is a package (no __path__ per importlib docs), and we
                    # encountered an error importing something. We cannot tell
                    # the difference between package.WrongNameTestClass and
                    # package.wrong_module_name so we just report the
                    # ImportError - it is more informative.
                    self.errors.append(error_message)
                    return error_case
                else:
                    # Otherwise, we signal that an AttributeError has occurred.
                    error_case, error_message = _make_failed_test(
                        part, e, self.suiteClass,
                        'Failed to access attribute:\n%s' % (
                            traceback.format_exc(),))
                    self.errors.append(error_message)
                    return error_case

        if isinstance(obj, types.ModuleType):
            return self.loadTestsFromModule(obj)
        elif isinstance(obj, type) and issubclass(obj, case.TestCase):
            return self.loadTestsFromTestCase(obj)
        elif (isinstance(obj, types.FunctionType) and
              isinstance(parent, type) and
              issubclass(parent, case.TestCase)):
            name = parts[-1]
            inst = parent(name)
            # static methods follow a different path
            if not isinstance(getattr(inst, name), types.FunctionType):
                return self.suiteClass([inst])
        elif isinstance(obj, suite.TestSuite):
            return obj
        if callable(obj):
            test = obj()
            if isinstance(test, suite.TestSuite):
                return test
            elif isinstance(test, case.TestCase):
                return self.suiteClass([test])
            else:
                raise TypeError("calling %s returned %s, not a test" %
                                (obj, test))
        else:
            raise TypeError("don't know how to make test from: %s" % obj)
  • unittest.TestLoader().loadTestsFromNames(多个方法,多个类)
    使用方法和loadTestsFromName(name,module=None)一样,一个列表推导式,创建了一个suites列表
    ,不同的是它可以接收一个说明符列表而不是一个,返回一个测试套件,包含所有说明符中的所有测试用例。
def loadTestsFromNames(self, names, module=None):
        """Return a suite of all test cases found using the given sequence
        of string specifiers. See 'loadTestsFromName()'.
        """
        suites = [self.loadTestsFromName(name, module) for name in names]
        return self.suiteClass(suites)
  • getTestCaseName(testCaseClass)
    返回一个有序的包含在TestCaseClass中的方法名列表。可以看做TestCase的子类。

  • unittest.TestLoader().discover() 探索性测试

从指定的start_dir(起始目录)递归查找所有子目录下的测试模块,并返回一个TestSuite对象。只有符合pattern模式匹配的测试文件才会被加载。模块名称必须有效才能被加载。

顶级项目中的所有模块必须是可导入的。如果start_dir不是顶级路径,那么顶级路径必须单独指出。

如果导入一个模块失败,比如由于语法错误,会被记录为一个单独的错误,然后discovery会继续。如果导入失败是由于设置了SkipTest,那么就会被记录为忽略测试。

当一个包(包含init.py文件的目录)被发现时,这个包会被load_tests方法检查。通过package.load_tests(loader,tests,pattern)方式调用。Test Discovery始终确保包在调用时只进行一次测试检查,即使load_tests方法自己调用了loader.discover。

如果load_tests方法存在,那么discovery就不再递归,load_tests会确保加载完所有的当前包下的测试。

pattern没有被特意存储为loader的属性,这样包就可以自行查找。top_level_dir被存储,所以load_tests就不需要再传参给loader.discover()。

start_dir可以是一个模块名,也可以是一个目录。

#pattern默认是以test开关的py文件
# start_dir开始进行搜索的目录(默认值为当前目录
def discover(self, start_dir, pattern='test*.py', top_level_dir=None):
        
        set_implicit_top = False
        if top_level_dir is None and self._top_level_dir is not None:
            # make top_level_dir optional if called from load_tests in a package
            top_level_dir = self._top_level_dir
        elif top_level_dir is None:
            set_implicit_top = True
            top_level_dir = start_dir

        top_level_dir = os.path.abspath(top_level_dir)

        if not top_level_dir in sys.path:
            # all test modules must be importable from the top level directory
            # should we *unconditionally* put the start directory in first
            # in sys.path to minimise likelihood of conflicts between installed
            # modules and development versions?
            sys.path.insert(0, top_level_dir)
        self._top_level_dir = top_level_dir

        is_not_importable = False
        is_namespace = False
        tests = []
        if os.path.isdir(os.path.abspath(start_dir)):
            start_dir = os.path.abspath(start_dir)
            if start_dir != top_level_dir:
                is_not_importable = not os.path.isfile(os.path.join(start_dir, '__init__.py'))
        else:
            # support for discovery from dotted module names
            try:
                __import__(start_dir)
            except ImportError:
                is_not_importable = True
            else:
                the_module = sys.modules[start_dir]
                top_part = start_dir.split('.')[0]
                try:
                    start_dir = os.path.abspath(
                       os.path.dirname((the_module.__file__)))
                except AttributeError:
                    # look for namespace packages
                    try:
                        spec = the_module.__spec__
                    except AttributeError:
                        spec = None

                    if spec and spec.loader is None:
                        if spec.submodule_search_locations is not None:
                            is_namespace = True

                            for path in the_module.__path__:
                                if (not set_implicit_top and
                                    not path.startswith(top_level_dir)):
                                    continue
                                self._top_level_dir = \
                                    (path.split(the_module.__name__
                                         .replace(".", os.path.sep))[0])
                                tests.extend(self._find_tests(path,
                                                              pattern,
                                                              namespace=True))
                    elif the_module.__name__ in sys.builtin_module_names:
                        # builtin module
                        raise TypeError('Can not use builtin modules '
                                        'as dotted module names') from None
                    else:
                        raise TypeError(
                            'don\'t know how to discover from {!r}'
                            .format(the_module)) from None

                if set_implicit_top:
                    if not is_namespace:
                        self._top_level_dir = \
                           self._get_directory_containing_module(top_part)
                        sys.path.remove(top_level_dir)
                    else:
                        sys.path.remove(top_level_dir)

        if is_not_importable:
            raise ImportError('Start directory is not importable: %r' % start_dir)

        if not is_namespace:
            tests = list(self._find_tests(start_dir, pattern))
        return self.suiteClass(tests)

这个可以把该测试类里的所有方法一起执行。

以下这些TestLoader的属性可以在子类和实例中配置:
testMethodPrefix
一种字符串,放在方法的名字前面时,该方法会被当做测试方法。默认一般是‘test’。
在getTestCaseNames()和所有的loadTestsFrom()方法中都有效。
sortTestMethodUsing
在getTestCaseNames()和所有的loadTestFrom
()方法中对测试方法进行排序时对测试方法名进行比较的函数。
suiteClass
从一个测试序列中构造出一个测试套件的可调用对象。生成的对象没有方法。默认值是TestSuite类。
对所有的loadTestsFrom*()方法有效。

你可能感兴趣的:(关于unittest单元测试框架)