unittest之TestCase类详解

TestCase

测试用例类,我们做单元测试时编写的测试用例就是继承TestCase类来实现具体的测试用例 \
例如:

import unittest
class UserCase(unittest.TestCase):

    def testAddUser(self):
        print("add a user")

    def testDelUser(self):
        print("delete a user")

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

可以这样理解:每一个继承TestCase类的子类里面实现的具体的方法(以test开头的方法)都是一条用例 \
既然我们写了用例,那用例又是如何被执行的呢:这就是下面要讲的TestCase类里面run方法,先贴一下代码:

    def run(self, result=None):
        orig_result = result
        #初始化result类,此类就是TestResult类或者其子类,用来记录用例的执行结果信息等
        if result is None:
            result = self.defaultTestResult()   
            startTestRun = getattr(result, 'startTestRun', None)
            if startTestRun is not None:
                startTestRun()

        self._resultForDoCleanups = result
        result.startTest(self)

        testMethod = getattr(self, self._testMethodName)    #获取当前TestCase类里面实现的某一个具体的方法(以test开头的方法)
        #判断TestCase类是否有被skip装饰,或类的testMethod方法有没有被skip装饰,如果有skip装饰,则直接跳过这个类或者这个方法不执行(包括后面tearDown也不会被执行),
        if (getattr(self.__class__, "__unittest_skip__", False) or
            getattr(testMethod, "__unittest_skip__", False)):
            # If the class or method was skipped.
            try:
                skip_why = (getattr(self.__class__, '__unittest_skip_why__', '')
                            or getattr(testMethod, '__unittest_skip_why__', ''))
                self._addSkip(result, skip_why)
            finally:
                result.stopTest(self)
            return
        #下面部分的代码逻辑是:执行setup部分,如果没有异常执行testMethod(即我们写的以test开头的TestCase类的方法),然后执行tearDown部分代码(不管testMethod是否通过)
        try:
            success = False
            try:
                self.setUp()
            except SkipTest as e:
                self._addSkip(result, str(e))
            except KeyboardInterrupt:
                raise
            except:
                result.addError(self, sys.exc_info())
            else:
                try:
                    testMethod()
                except KeyboardInterrupt:
                    raise
                except self.failureException:
                    result.addFailure(self, sys.exc_info())
                except _ExpectedFailure as e:
                    addExpectedFailure = getattr(result, 'addExpectedFailure', None)
                    if addExpectedFailure is not None:
                        addExpectedFailure(self, e.exc_info)
                    else:
                        warnings.warn("TestResult has no addExpectedFailure method, reporting as passes",
                                      RuntimeWarning)
                        result.addSuccess(self)
                except _UnexpectedSuccess:
                    addUnexpectedSuccess = getattr(result, 'addUnexpectedSuccess', None)
                    if addUnexpectedSuccess is not None:
                        addUnexpectedSuccess(self)
                    else:
                        warnings.warn("TestResult has no addUnexpectedSuccess method, reporting as failures",
                                      RuntimeWarning)
                        result.addFailure(self, sys.exc_info())
                except SkipTest as e:
                    self._addSkip(result, str(e))
                except:
                    result.addError(self, sys.exc_info())
                else:
                    success = True

                try:
                    self.tearDown()
                except KeyboardInterrupt:
                    raise
                except:
                    result.addError(self, sys.exc_info())
                    success = False

            cleanUpSuccess = self.doCleanups()
            success = success and cleanUpSuccess
            if success:
                result.addSuccess(self)
        #此部分是用例执行完成之后的收尾工作
        finally:
            result.stopTest(self)
            if orig_result is None:
                stopTestRun = getattr(result, 'stopTestRun', None)
                if stopTestRun is not None:
                    stopTestRun()

注意:run方法的第一个参数是result
下面详细说明一下run方法里面得逻辑:

orig_result = result
if result is None:
            result = self.defaultTestResult()   
            startTestRun = getattr(result, 'startTestRun', None)
            if startTestRun is not None:
                startTestRun()

self._resultForDoCleanups = result
result.startTest(self)

这一部分代码是用来实例化一个TestResult类,如果run方法没有传入result参数,则用self.defaultTestResult方法返回一个TestResult类的实例,并且如果TestResult如果有startTestRun方法就执行startTestRun方法。后面两句中的第一句是将result赋值TestCase实例_resultForDoCleanups属性,这个会在后面再说,第二句是执行result的startTest方法(可以看一下TestResult的startTest方法)。

继续往下看代码:

testMethod = getattr(self, self._testMethodName)
if (getattr(self.__class__, "__unittest_skip__", False) or
    getattr(testMethod, "__unittest_skip__", False)):
    # If the class or method was skipped.
    try:
        skip_why = (getattr(self.__class__, '__unittest_skip_why__', '')
                    or getattr(testMethod, '__unittest_skip_why__', ''))
        self._addSkip(result, skip_why)
    finally:
        result.stopTest(self)
    return

第一句代码:testMethod = getattr(self, self._testMethodName),这句主要获取TestCase类里面的方法,而self._testMethodName就是我们编写测试类里面的的方法(以test开头的方法)。比如上面例子中的testAddUser方法\
后面部分代码是判断当前我们写的TestCase类是否有unittest_skip,或者我们写的TestCase类的方法(以test开头)是否有unittest_skip方法,如果有则获取类的unittest_skip_why属性或者方法的unittest_skip_why属性。并在result中添加skip的原因。之后执行result的stopTest方法,并直接返回不继续执行下去(==包括setUp和teardown都不会执行==)。那么我们如何去实现这skip的逻辑呢,举例如下:

1、TestCase带有__unittest_skip__属性:
@unittest.skip('class skip')
class UnitTestCase(unittest.TestCase):
    def test01(self):
        print('test01')
    def test02(self):
        print('test02')
    def tearDown(self):
        print('teardown')
if __name__ == '__main__':
    unittest.main()

2、方法带有__unittest_skip__属性
@unittest.skip('class skip')
class UnitTestCase(unittest.TestCase):
    @unittest.skip('skip')
    def test01(self):
        print('test01')
    @unittest.skip('skip')
    def test02(self):
        print('test02')
    def tearDown(self):
        print('teardown')
if __name__ == '__main__':
    unittest.main()

unittest_skipunittest_skip_why这两个属性是如何给类或方法带上的呢,那让我们来看一下skip方法。先贴代码:

def skip(reason):
    """
    Unconditionally skip a test.
    """
    def decorator(test_item):
        if not isinstance(test_item, (type, types.ClassType)):
            @functools.wraps(test_item)
            def skip_wrapper(*args, **kwargs):
                raise SkipTest(reason)
            test_item = skip_wrapper

        test_item.__unittest_skip__ = True
        test_item.__unittest_skip_why__ = reason
        return test_item
    return decorator

从代码中可以看出skip方法会给test_item(也就是类名或者方法名)增加两个属性unittest_skipunittest_skip_why并分别赋值为True和reason。

我们继续上面的run方法剩下部分的代码分析:如果没有skip装饰类或方法,则执行下面的逻辑:

try:
    success = False
    try:
        self.setUp()
    except SkipTest as e:
        self._addSkip(result, str(e))
    except KeyboardInterrupt:
        raise
    except:
        result.addError(self, sys.exc_info())
    else:
        try:
            testMethod()
        except KeyboardInterrupt:
            raise
        except self.failureException:
            result.addFailure(self, sys.exc_info())
        except _ExpectedFailure as e:
            addExpectedFailure = getattr(result, 'addExpectedFailure', None)
            if addExpectedFailure is not None:
                addExpectedFailure(self, e.exc_info)
            else:
                warnings.warn("TestResult has no addExpectedFailure method, reporting as passes",
                              RuntimeWarning)
                result.addSuccess(self)
        except _UnexpectedSuccess:
            addUnexpectedSuccess = getattr(result, 'addUnexpectedSuccess', None)
            if addUnexpectedSuccess is not None:
                addUnexpectedSuccess(self)
            else:
                warnings.warn("TestResult has no addUnexpectedSuccess method, reporting as failures",
                              RuntimeWarning)
                result.addFailure(self, sys.exc_info())
        except SkipTest as e:
            self._addSkip(result, str(e))
        except:
            result.addError(self, sys.exc_info())
        else:
            success = True

        try:
            self.tearDown()
        except KeyboardInterrupt:
            raise
        except:
            result.addError(self, sys.exc_info())
            success = False

    cleanUpSuccess = self.doCleanups()
    success = success and cleanUpSuccess
    if success:
        result.addSuccess(self)
finally:
    result.stopTest(self)
    if orig_result is None:
        stopTestRun = getattr(result, 'stopTestRun', None)
        if stopTestRun is not None:
            stopTestRun()

我们分解开来分析,第一部分

try:
    success = False
    self.setUp()
    except SkipTest as e:
        self._addSkip(result, str(e))
    except KeyboardInterrupt:
        raise
    except:
        result.addError(self, sys.exc_info())

在用例执行之前给是否成功的标识赋值为false,success=False \
接着执行setUp方法,如果是在setUp中skipTest异常抛出,则在result中添加异常信息。如何实现这个SkipTest异常呢,我们可以在setUp方法中增加一句代码:raise unittest.SkipTest(‘setup skip test’),如果是在setUp中遇到KeyboardInterrupt异常(Ctrl+c),则会直接抛出异常,如果是除了这两个异常之外的其他异常,则在result中添加error信息。

第二部分

else:
    try:
        testMethod()
    except KeyboardInterrupt:
        raise
    except self.failureException:
        result.addFailure(self, sys.exc_info())
    except _ExpectedFailure as e:
        addExpectedFailure = getattr(result, 'addExpectedFailure', None)
        if addExpectedFailure is not None:
            addExpectedFailure(self, e.exc_info)
        else:
            warnings.warn("TestResult has no addExpectedFailure method, reporting as passes",
                          RuntimeWarning)
            result.addSuccess(self)
    except _UnexpectedSuccess:
        addUnexpectedSuccess = getattr(result, 'addUnexpectedSuccess', None)
        if addUnexpectedSuccess is not None:
            addUnexpectedSuccess(self)
        else:
            warnings.warn("TestResult has no addUnexpectedSuccess method, reporting as failures",
                          RuntimeWarning)
            result.addFailure(self, sys.exc_info())
    except SkipTest as e:
        self._addSkip(result, str(e))
    except:
        result.addError(self, sys.exc_info())
    else:
        success = True

    try:
        self.tearDown()
    except KeyboardInterrupt:
        raise
    except:
        result.addError(self, sys.exc_info())
        success = False

cleanUpSuccess = self.doCleanups()
success = success and cleanUpSuccess
if success:
    result.addSuccess(self)

如果是setup部分没有报错或异常,则执行我们的testMethod方法(即我们写的具体的用例方法,以test开头),接下来的代码都是处理异常,else部分是如果没有异常则把success标识置为true,表示用例执行通过,然后执行tearDown部分的代码,如果tearDown部分遇到异常了,success标识又置为false。

第三部分:

finally:
    result.stopTest(self)
    if orig_result is None:
        stopTestRun = getattr(result, 'stopTestRun', None)
        if stopTestRun is not None:
            stopTestRun()

执行result的stopTest和stopTestRun方法(如果有)\
至此,run方法分析完成了。

上面我们介绍了run方法,那又是什么时候会调用run方法呢,再看TestCase的类里面方法,发现有一个call魔术方法:

def __call__(self, *args, **kwds):
    return self.run(*args, **kwds)

会去调我们的run方法(如果不太明白call方法是如何使用的,请百度一下),这就让我们知道肯定有一个地方调用了类似这样一个方法(以上面的例子为例):TestCase(‘testAddUser’)(result),这就是我的下一篇分析unittest之TestSuite类说明要讲的内容了

我们根据上面的分析可以来举一个列子来试验一下:

import unittest
class UserCase(unittest.TestCase):

    def testAddUser(self):
        print("add a user")

    def testDelUser(self):
        print("delete a user")

if __name__ == '__main__':
    result = unittest.TextTestResult(sys.stdout,'test result',1) #初始化TextTestResult类实例
    testcase = UserCase('testAddUser')  #初始化UserCase类实例
    testcase(result) #跟testcase.run(result)的结果是一样的,我们只需要传入一个result对象即可

运行结果如下:

E:\PythonWorkSpace>python test.py
add a user
.

E:\PythonWorkSpace>

可以运行成功,说明跟我们的分析一直,我们可以直接通过TestCase的run方法运行我们的用例,或者用过它的call魔术方法也是可以的,即上面例子中的testcase(result)就是通过此魔术方法去调用的run方法。


版权声明:本文为fengguangke原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。