> 백엔드 개발 > 파이썬 튜토리얼 > Python의 단위 테스트 및 확장된 HTMLTestRunner에 대한 자세한 설명

Python의 단위 테스트 및 확장된 HTMLTestRunner에 대한 자세한 설명

高洛峰
풀어 주다: 2017-03-23 15:11:58
원래의
5132명이 탐색했습니다.

unnitest는 Python 개발을 위한 Python의 중요한 단위 테스트 프레임워크입니다. 이를 사용하기 위해 깊이 있는 이해가 필요하지는 않지만, 자동화 테스터는 unnitest의 실행 원리와 관련 모듈의 기능에 대해 잘 알고 있어야 한다고 생각합니다. 다음과 같은 몇 가지 간단한 요구 사항을 언급하고 싶습니다. 🎜>

1. 단위 테스트 대신 unnitest를 사용하여 프로세스 테스트를 수행하는 방법 예를 들어 selenium+unnitest를 사용하여 test1에 로그인을 구현하고 test2는 이를 기반으로 쿼리를 구현합니다. test1의 성공적인 로그인, test3에서 일부 데이터를 쿼리한 후 페이지가 선택적으로 데이터를 제출합니다. test1에 로그인한 후 브라우저를 닫고 쿼리를 실행한 다음 브라우저를 닫습니다. 아니면 테스트에서 이 단계를 다시 작성해도 되지만, 계속해서 잘라내고 정리하는 과정이 너무 길면 어떡하지 않나요....

2. unnitest의 실행 순서를 제어합니다. unnitest의 테스트 배열에 저장된 TestCase는 기본적으로 첫 번째 문자로 정렬됩니다. test1, test2...test9의 실행 순서에는 문제가 없습니다. test1, test2........test18과 같은 여러 테스트의 경우 unnitest는 이 순서가 아닙니다. 알파벳 순서로 test1, test10,...test18, test2를 실행합니다. ... .....test9는 물론 내가 언급한 내용이 단위 테스트에 거의 영향을 미치지 않지만(나중에 언급되는 테스트 간에 데이터 종속성이 없는 한) 프로세스에 지장을 줄 수 있습니다. 3. 프로세스 테스트 중에 특정 테스트 실행을 건너뛸지 여부를 동적으로 제어하는 ​​방법은 프로세스에 대한 일반적인 아이디어이기도 합니다. 예를 들어, 테스트1에 대한 로그인에도 성공하지 못했습니다. 테스트? 그 이후에 보고되는 모든 오류는 NosuchElement 오류입니다. 이러한 오류는 의미가 없으며 시간 낭비입니다.... 보고서도 "인도적"이지 않으므로 이제 우리에게 좋은 아이디어는 다음과 같습니다. test1이 성공적으로 실행되지 않는 경우 . 를 사용하면 모든 후속 테스트를 동적으로 건너뛸 수 있습니다. 실제로는 test1뿐만 아니라 후속 테스트도 건너뛰게 됩니다. 정확히 말하면 test1, test2...testN의 테스트가 실패하면 후속 테스트도 건너뜁니다. 물론 다음 테스트가 이전 테스트와 관련이 없는 경우 이전 테스트의 성공 여부와 관계없이 건너뛰지 않도록 선택할 수도 있고 test1 로그인에만 관련될 수도 있습니다. 로그인이 성공하는 한 건너뛰지 않겠습니다. 로그인에 실패하면 건너뛰겠습니다. 더 많은 것이 있습니다. 더 중요한 것은 보고서에 건너뛴 것으로 표시될 수 없다는 것입니다. 특정 사용 사례를 사용하면 보고서가 표시되지 않으므로 상사는 귀하가 게으르다고 생각할 것입니다. 사용 사례가 너무 적습니까? 이전 Use Case가 실패해서 표시되지 않는다고 설명하려면 아직도 떨려야 하는데...

저희 하이엔드는 이렇게 test1 실행 실패, test2... testN, 반영되어 있습니다. 보고서에는 해당 사례가 건너뛰기로 표시되어 있으며 클릭하면 "test1이 성공적으로 실행되지 않았으므로 이 사례를 건너뛰었습니다."라는 이유에 대한 설명이 있습니다. 이것이 HTMLTestRunner를 확장한 이유입니다. 한 줄씩 확장하는 방법은 나중에 설명하겠습니다.

아이디어는 많은데 어떻게 구현할까요? 그럼 파이썬의 unnitest에 대해 더 자세히 살펴보겠습니다!

겉으로는 복잡해 보이는 unnitest에 대해서는 unnitest=TestCase+TestResult를 알려드리겠습니다. 이 두 모듈에 익숙하다면 "원하는 것은 무엇이든 할 수 있습니다"! ! TestSuite, TextTestRunner 등이 없다고 하시는 분들도 계시겠지만, 사실 우리 대부분이 이런 모듈을 주로 사용하는 것이 사실이지만, 사실 최종적으로 실행되는 것은 TestCase의 run 메소드이고, 그 결과는 다음과 같습니다. TestResult(또는 하위 클래스)에 제공됩니다. 먼저 간단한 unnitest 예제를 살펴보고 이를 확장해 보겠습니다. 예는 다음과 같습니다:

import unittest
class Mydemo(unittest.TestCase):
    def setUp(self):
        self.a=1
    def test1(self):
        print "i am test1 the value of a is {}".format(self.a)
    def test2(self):
        print "i am test2 the value of a is {}".format(self.a)
    def  test3(self):
        print "i am test3 the value of a is {}".format(self.a)
if __name__ == '__main__':
    unittest.main()
로그인 후 복사

실행 결과는 다음과 같습니다:

i am test1 the value of a is 1
...
i am test2 the value of a is 1
i am test3 the value of a is 1
----------------------------------------------------------------------
Ran 3 tests in 0.000s

OK
로그인 후 복사

이는 문제가 되지 않습니다. 그러면 unnitest.main()이 무엇인지 생각해 볼 수 있습니다. 다른 방법으로 작성하면 test1, test2만 실행할 수 있고 test3은 실행할 수 없나요? (당분간 건너뛰지 않고) 그런 다음 unittest.main()에서 이를 살펴봅니다. 디버깅 후 최종 실행은 TestProgram입니다. 코드의

Constructor

부분을 게시하세요.

if argv is None:
            argv = sys.argv#得到当前模块的绝对路径

        self.exit = exit
        self.failfast = failfast
        self.catchbreak = catchbreak
        self.verbosity = verbosity
        self.buffer = buffer
        self.defaultTest = defaultTest
        self.testRunner = testRunner
        self.testLoader = testLoader
        self.progName = os.path.basename(argv[0])
        self.parseArgs(argv)#查找当前module的Testsuite
        self.runTests()#执行测试
로그인 후 복사

자, 위에서 보면 실제로 두 가지 주요 단계만 있음을 알 수 있습니다. 하나: 찾기 테스트할 테스트 케이스를 Testsuite에 추가합니다. 두 번째: Testsuite를 실행하고 결과를 TestResult에 제공합니다.

먼저, TestCase가 무엇인지 이해하세요. TestSuite란 무엇입니까? 둘째: 이러한 테스트케이스 또는 TestSuite를 찾는 방법은 무엇입니까?

TestCase란 무엇인가요?

어떤 사람들은 TesetCase가 test로 시작하면 테스트케이스라고 말합니다. 정확히 말하면 다음과 같이 TesetCase 클래스를 인스턴스화하는 TestCase입니다. >
import unittest
class Mydemo(unittest.TestCase):
    def setUp(self):
        self.a=1
    def Mytest1(self):
        print "i am Mytest1 the value of a is {}".format(self.a)
    def Mytest2(self):
        print "i am Mytest2 the value of a is {}".format(self.a)
    def Mytest3(self):
        print "i am Mytest3 the value of a is {}".format(self.a)
if __name__ == '__main__':
    test_runner=unittest.TextTestRunner()
    test_suit=unittest.TestSuite()
    test_suit.addTests(map(Mydemo,["Mytest1","Mytest2","Mytest3"]))
    test_runner.run(test_suit)
로그인 후 복사

실행 결과는 다음과 같습니다.

...
i am Mytest1 the value of a is 1
----------------------------------------------------------------------
i am Mytest2 the value of a is 1
Ran 3 tests in 0.000s
i am Mytest3 the value of a is 1
 
OK
로그인 후 복사

上面3个Testcase可并没有以test开头...那么为什么大家都要默认以test开头来写呢,我们打开C:\Python27\Lib\unittest\loader.py这个模块在296行有写defaultTestLoader = TestLoader(),我们来看看TestLoader这个类第一行就看见testMethodPrefix = 'test',也就是说如果你使用到defaultTestLoader,那么默认是以test开头的方法为一个用例,具体可以在TestLoader类中的getTestCaseNames得到实现,红字注释部分为什么testCaseClass要有call方法,我们后面提到。(不知道call这个魔法属性的用法自行百度)

def getTestCaseNames(self, testCaseClass):
        """Return a sorted sequence of method names found within testCaseClass
        """
        def isTestMethod(attrname, testCaseClass=testCaseClass,
                         prefix=self.testMethodPrefix):
            return attrname.startswith(prefix) and \
                hasattr(getattr(testCaseClass, attrname), '__call__')#返回一个testCaseClass有__call__方法且attrname以prefix开头的为一个testcase
        testFnNames = filter(isTestMethod, dir(testCaseClass))
        if self.sortTestMethodsUsing:
            testFnNames.sort(key=_CmpToKey(self.sortTestMethodsUsing))
        return testFnNames
로그인 후 복사

原来是这样啊,我们上文提到的unittest.main()其实用的就是defaultTestLoader,当然你把if name == 'main'下面的代码换成unittest.main()肯定不成功,除非你把上文提到的testMethodPrefix 换成"Mytest"。有了对TestCase的看法,我们具体来看看这个类。

这个类里面包含了我们所能用的方法。我列出来一些主要的吧。

setUp()在每个test执行前都要执行的方法。

tearDown()在每个test执行后都要执行的方法。(不管是否执行成功)

setUpClass()在一个测试类中在所有test开始之前,执行一次且必须使用到Testsuite(只有在TestSuite的run方法里面才对其调用)

tearDownClass()在一个测试类中在所有test结束之后,执行一次且必须使用到Testsuite(只有在TestSuite的run方法里面才对其调用)

run()这是unnitest的核心,逻辑也相对复杂,但是很好理解,具体自己看源码。所有最终case的执行都会归结到该run方法。

还有一个重要的_resultForDoCleanups私有变量,存储TestResult的执行结果,这个在构建后面的skip用到。

我们要明确TestCase类中所有的测试用例是独立的,我上面说过了,其实每个testcase就是一个个TestCase类的实例对象,所以不要企图在某个test存储或改变一个变量,下个test中能用到,除非利用到setUpClass。我们看个例子:

import unittest
class Mydemo(unittest.TestCase):
    def test1(self):
        self.a=1
        print "i am test1 the value of a is {}".format(self.a)
    def test2(self):
        print "i am test2 the value of a is {}".format(self.a)
if __name__ == '__main__':
    unittest.main()
로그인 후 복사

结果:

C:\Python27\python.exe D:/Moudle/module_1/test4.py
i am test1 the value of a is 1
.E
======================================================================
ERROR: test2 (__main__.Mydemo)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "D:/Moudle/module_1/test4.py", line 7, in test2
    print "i am test2 the value of a is {}".format(self.a)
AttributeError: 'Mydemo' object has no attribute 'a'

----------------------------------------------------------------------
Ran 2 tests in 0.001s

FAILED (errors=1)
로그인 후 복사

上面就是说明TestCase类中所有的测试用例是独立的,每个testcase就是由TestCase实例化的一个独立的实例。那是不是就是每个TestCase不能共享数据呢?答案是否定的,不能共享的原因是我们上面用到的是self(实例对象属性),能共享我们就必须使用类属性,比如下个例子:

import unittest
class Mydemo(unittest.TestCase):
    def test1(self):        Mydemo.a=1
        print "i am test1 the value of a is {}".format(self.a)
    def test2(self):
        print "i am test2 the value of a is {}".format(Mydemo.a)
if name == 'main':
    unittest.main()
로그인 후 복사

运行结果如下:

i am test1 the value of a is 1
..
i am test2 the value of a is 1
----------------------------------------------------------------------
Ran 2 tests in 0.000s

OK
로그인 후 복사

这些东西其实是python类的一些动态行为,但是既然和unnitest关联,就随便提下。我们运行test1的时候,给Mydemo加了一个新的属性a(值为1),当我们运行test2时,我们就能拿到Mydemo类的属性了。说了TaseCase我们不得不说下TestSuite。TestSuite是有一个个TestCase组成的,当然TestSuite里面可以再嵌套TestSuite。我们打开C:\Python27\Lib\unittest\suite.py找到TestSuite,它继承于BaseTestSuite,其实主要的一些属性就那么几个:

1.self._tests这个私有变量里面方的是所有的TestCase或者TestSuite。

2.run()方法,方法如下:

def run(self, result, debug=False):
        topLevel = False
        if getattr(result, '_testRunEntered', False) is False:
            result._testRunEntered = topLevel = True
 
        for test in self:#这个循环会一直遍历_tests中的变量
            if result.shouldStop:
                break
            if _isnotsuite(test):
                self._tearDownPreviousClass(test, result)
                self._handleModuleFixture(test, result)
                self._handleClassSetUp(test, result)#这一句提到了调用setUpClass的规则
                result._previousTestClass = test.__class__
 
                if (getattr(test.__class__, '_classSetupFailed', False) or
                    getattr(result, '_moduleSetUpFailed', False)):
                    continue
 
            if not debug:
                test(result)#如果是TestSuit继续调用该方法,如果是TestCase则调用TestCase中的run方法
            else:
                test.debug()
 
        if topLevel:
            self._tearDownPreviousClass(None, result)
            self._handleModuleTearDown(result)
            result._testRunEntered = False
        return result
로그인 후 복사

注释1:self是个迭代对象,一直遍历上文提到的self._tests变量

注释2:我们看看_handleClassSetUp中的方法,发现在在用例的执行过程中,每个TestCase类只会调用一次setUpClass方法,同理tearDownClass。对用这一点我们举个例子:

import unittest
class Mydemo(unittest.TestCase):
    @classmethod
    def setUpClass(cls):
        print "I am setUpClass"
    def test1(self):
        print "i am test1 "
    def test2(self):
        print "i am test2"
    @classmethod
    def tearDownClass(cls):
        print "I am tearDownClass"
if __name__ == '__main__':
    unittest.main()
로그인 후 복사

运行结果是:

C:\Python27\python.exe D:/Moudle/module_1/test4.py
I am setUpClass
..
i am test1 
----------------------------------------------------------------------
i am test2
Ran 2 tests in 0.001s
I am tearDownClass

OK
로그인 후 복사

说明类方法setUpClass与tearDownClass只执行了一遍了,这就回答了我们第一个问题了:在setUpClass中启动浏览器,执行完所有流程后关闭浏览器,举一个简单的demo就是:

#coding=utf-
import unittest
from  selenium import webdriver
class Mydemo(unittest.TestCase):
    @classmethod
    def setUpClass(cls):
        cls.browser=webdriver.Firefox()
    def test1(self):
        '''登录'''
        browser=self.browser
        #do someting about login
    def test2(self):
        '''查询'''
        browser = self.browser
        # do someting about search
    def test3(self):
        '''提交数据'''
        browser = self.browser
        # do someting about submmit
    @classmethod
    def tearDownClass(cls):
        browser=self.browser
        browser.close()
    
if name == 'main':
    unittest.main()
로그인 후 복사

上面就会在所有的case执行之前启动firefox,因为每个test中拿到的都是Mydemo类中同一个webdriver对象,所以能保证操作的都是同一个浏览器句柄。关于这个setUpClass如果想要动态的改变某个值一定要使用python的可变的对象比如list,dict等...这些其实都是一些python类的一些知识,算我啰嗦吧我还是想举个例子,嫌烦的同学,绕过这一部分吧。

#coding=utf-8
import unittest
from  selenium import webdriver
class Mydemo(unittest.TestCase):
    @classmethod
    def setUpClass(cls):
        cls.a=1
    def test1(self):
        print "before update the a in test1 is:{}".format(self.a)
        self.a=self.a+1
        print "after update the a in test1 is:{}".format(self.a)
    def test2(self):
        print "the value in test2 is:{}".format(self.a)
    @classmethod
    def tearDownClass(cls):
        print "I am tearDownClass"
if name == 'main':
    unittest.main()
로그인 후 복사

运行结果:

C:\Python27\python.exe D:/Moudle/module_1/test4.py
before update the a in test1 is:1
..
after update the a in test1 is:2
----------------------------------------------------------------------
the value in test2 is:1
I am tearDownClass
Ran 2 tests in 0.001s

OK
로그인 후 복사

我们想在test1中改变a的值,但是test2中的结果说明a没有被改变,这其实也很好理解。如果我们想要改变怎么办,看看下面的例子:

#coding=utf-8
import unittest
from  selenium import webdriver
class Mydemo(unittest.TestCase):
    @classmethod
    def setUpClass(cls):
        cls.a=[0]
    def test1(self):
        print "before update the a in test1 is:{}".format(self.a[0])
        self.a[0]=self.a[0]+1
        print "after update the a in test1 is:{}".format(self.a[0])
    def test2(self):
        print "the value in test2 is:{}".format(self.a[0])
    @classmethod
    def tearDownClass(cls):
        print "I am tearDownClass"
if name == 'main':
    unittest.main()
로그인 후 복사

运行结果:

C:\Python27\python.exe D:/Moudle/module_1/test4.py
..
before update the a in test1 is:0
----------------------------------------------------------------------
after update the a in test1 is:1
Ran 2 tests in 0.000s
the value in test2 is:1

I am tearDownClass
OK
로그인 후 복사

我们把a变成一个list,发现a的值在test2中改变了。好了这一部分就这样了。

注释三:这个其实也是python类的一些知识可能有的人没有关注就是call这个魔法属性,我们看到在这个循环中test如果是testsuite对象,那么会调用中TestSuite类中的call方法(在其父类BaseTestSuite中),该方法中会再次调用run方法。一直到test是个testcase对象,那么就会调用我们上文提到的TestCase中的call(这就是我们上面提到为什么找有call属性类实例的方法),一样该call中的方法也是调用TestCase中的run。所以最终所有的执行其实都是执行TestCase中的run方法。

上面大致讲了一些TestCase与TestSuit的知识,可能穿插的比较多。

如何创建这些Testcase或者TestSuite?

1.自己手动实例化TestCase

这个上面已经有例子,与普通类无异,这中在自动化领域用处不大,我们不能一个个的实例化吧...

2.利用C:\Python27\Lib\unittest\loader.py模块的TestLoader,该类提供了多种不同情境find testcase。

1.loadTestsFromTestCase利用给出的TestCase类名称返回找到所有的suite。

2.loadTestsFromMoudle利用给出的Moudle返回找到所有的suite。

3.loadTestsFromName利用给出的Moudle名称返回找到所有的suite。

4.discover返回给定目录下符合pattern类型(默认test*.py)所有的suite。

其实这些方法最终都要归结到loadTestsFromTestCase,可能官方不提供我们也能写,既然有了就直接用吧。

经过上面的说明,我觉得大家对一TestCase,TestSuite应该有一个比较清楚的认识了,也解决了我自己的提问。问题一:我们可以用类方法setUpClass实现。对于问题二:我们可以利用TestLoader类中的方法返回suite,然后对这些suite按照自己的想法进行一些排序,然后再调用run方法。说完了TestCase我们再说下TestResult。

什么是TestResult?

顾名思义,testresult就是存储测试结果的,不过通过何种方式调用run函数,最终到Testcase中的run方法时必须传一个result(如果为None则自己实例化一个TestResult对象)。这个result就是TestResult对象或者是其子类的对象,我们每次执行的结果都会调用其addFailure,addSuccess,addSkip....等方法将执行结果保存到TestResult实例属性中。我们还是来看看TestCase的run方法:

def run(self, result=None):
        orig_result = result
        if result is None:#如果没有传入result对象自己实例化一个TestResult对象
            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)
        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)#调用addSkip
            finally:
                result.stopTest(self)
            return
        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())#调用addError
            else:
                try:
                    testMethod()
                except KeyboardInterrupt:
                    raise
                except self.failureException:
                    result.addFailure(self, sys.exc_info())#调用addFailure
                except _ExpectedFailure as e:
                    addExpectedFailure = getattr(result, 'addExpectedFailure', None)
                    if addExpectedFailure is not None:
                        addExpectedFailure(self, e.exc_info)#调用addExpectedFailure
                    else:
                        warnings.warn("TestResult has no addExpectedFailure method, reporting as passes",
                                      RuntimeWarning)
                        result.addSuccess(self)#调用addSuccess
                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()
로그인 후 복사

通过注释部分我们可以看出,每次执行用例时,都会把执行结果保存到TestResult中。我们再看看TextTestRunner这个类,在开始就使用了类TextTestResult,而这个类也是继承TestResult,而后在执行的过程中最终把TextTestResult实例对象传递给TestCase的run方法。所以我上文说了,不过你是用什么方式执行unnitest,到最后都是TestCase的run方法与TestResult的游戏。而我们的HTMLTestRunner模块也是在继承在TestResult类的基础上的。

说完了TestCase我们来看看第三个问题吧,也是比较有实际意义的话题,开始我是这样跳过某些test的,代码是这样的:

#coding=utf-8
import unittest
a=[False]
class Mydemo(unittest.TestCase):
    def test1(self):
        try:
            print "i am test1"
            #test 1 do some thing
        except Exception,e:
            a[0] = True
            raise e
    @unittest.skipIf(a[0],"test1 fail skip test2")
    def test2(self):
        try:
            print "i am test2"
            raise  AssertionError("error")
            # test2 do some thing
        except Exception,e:
            a[0] = True
            raise e
    @unittest.skipIf(a[0], "test1 fail skip test2")
    def test3(self):
        try:
            print "i am test3"
            # test2 do some thing
        except Exception, e:
            a[0] =True
            raise e
if __name__ == '__main__':
    unittest.main()
로그인 후 복사

想法很简单:就是利用一个全局的数组,如果某个test执行出错我就更改这个数组元素,到下一个case执行的时候就会判断是否要跳过。上面因为test2出错了,原本我们想跳过test3,但是很遗憾并没有跳过test3!结果如下:

C:\Python27\python.exe D:/Moudle/module_1/test4.py
i am test1
.F.
i am test2
======================================================================
i am test3
FAIL: test2 (__main__.Mydemo)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "D:/Moudle/module_1/test4.py", line 20, in test2
    raise e
AssertionError: error

----------------------------------------------------------------------
Ran 3 tests in 0.000s

FAILED (failures=1)
로그인 후 복사

原因很简单:python在创建Mydemo这个类的时候,由于实例方法都使用了装饰器unittest.skipIf,所以每个方法都向unittest.skipIf这个装饰传递传递参数a[0],但是这个a[0]是没用执行过任何case之前的a[0],也就是我们刚开始定义的a[0]=Flase,所以不可能跳过的。退一万步讲,即使这样可行,也太不美观了吧。我们想的是当执行当前的test时能判断前面是否有出错的case,有的话就跳过了。可行吗?我觉得可行。主要就是用到我上面提到的TestCase中的_resultForDoCleanups的变量,这个其实就是TestResult一个引用。那么我们可以这样写:

#coding=utf-8
import unittest
class Mydemo(unittest.TestCase):
    def test1(self):
        print "excute test1"
    def test2(self):
        if self._resultForDoCleanups.failures or self._resultForDoCleanups.errors:
            raise unittest.SkipTest("{} do not excute because {} is failed".format(self._testMethodName,self._resultForDoCleanups.failures[0][0]._testMethodName))
        print "excute test2"
        raise AssertionError("test2 fail")

    def test3(self):
        if self._resultForDoCleanups.failures or self._resultForDoCleanups.errors:
            raise unittest.SkipTest("{} do not excute because {} is failed".format(self._testMethodName,self._resultForDoCleanups.failures[0][0]._testMethodName))
        print "excute test3"
if name == 'main':
    unittest.main()
로그인 후 복사

运行结果如下:

.Fs
======================================================================
FAIL: test2 (main.Mydemo)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "D:/Moudle/module_1/test4.py", line 10, in test2
    raise AssertionError("test2 fail")
AssertionError: test2 fail

----------------------------------------------------------------------
Ran 3 tests in 0.001s

FAILED (failures=1, skipped=1)
excute test1
excute test2
로그인 후 복사

可以了,我们看出,test2失败了,test3跳过;当然test2如果正确,test3会执行。目的是达到了,可是每个case都这样写不太好,我们想到了装饰器(不会自行百度),在C:\Python27\Lib\unittest\case.py中新增如下代码:

def Myskip(func):
    def RebackTest(self):
        if self._resultForDoCleanups.failures or self._resultForDoCleanups.errors:
            raise unittest.SkipTest("{} do not excute because {} is failed".format(func.name,self._resultForDoCleanups.failures[0][0]._testMethodName))
        func(self)
    return  RebackTest
로그인 후 복사

然后C:\Python27\Lib\unittest\init.py中新增:

all = ['TestResult', 'TestCase', 'TestSuite',
           'TextTestRunner', 'TestLoader', 'FunctionTestCase', 'main',
           'defaultTestLoader', 'SkipTest', 'skip', 'skipIf', 'skipUnless',
           'expectedFailure', 'TextTestResult', 'installHandler',
           'registerResult', 'removeResult', 'removeHandler','Myskip']
......
from .case import (TestCase, FunctionTestCase, SkipTest, skip, skipIf,Myskip,
                   skipUnless, expectedFailure)
로그인 후 복사

最终我们这样写:

#coding=utf-8
import unittest
class Mydemo(unittest.TestCase):
    def test1(self):
        print "excute test1"
    @unittest.Myskip
    def test2(self):
        print "excute test2"
        raise AssertionError("test2 fail")
    @unittest.Myskip
    def test3(self):
        print "excute test3"
if name == 'main':
    unittest.main()
로그인 후 복사

好了,看上去还不错....关于其他的unnitest相关知识,不想再扯了,最后拓展HTMLTestRunner报告,这可能是大家关心的!写这个HTMLTestRunner的大神是在好久之前的了,基本能满足大家需求。但是,目前对于web自动化,我觉得至少要新增2个东西。第一个新增skip列:因为我可能会skip某些case;第二新增截图列,如果有错误我可能要截图。
打了这么久字不想再多说了....我给出全部代码,然后代码中我改变的地方我给出标记并加注释吧,完整代码如下:(可能有点长,但是要有点耐心)

#coding=utf-8
"""
A TestRunner for use with the Python unit testing framework. It
generates a HTML report to show the result at a glance.

The simplest way to use this is to invoke its main method. E.g.

    import unittest
    import HTMLTestRunner

    ... define your tests ...

    if name == 'main':
        HTMLTestRunner.main()


For more customization options, instantiates a HTMLTestRunner object.
HTMLTestRunner is a counterpart to unittest's TextTestRunner. E.g.

    # output to a file
    fp = file('my_report.html', 'wb')
    runner = HTMLTestRunner.HTMLTestRunner(
                stream=fp,
                title='My unit test',
                description='This demonstrates the report output by HTMLTestRunner.'
                )

    # Use an external stylesheet.
    # See the Template_mixin class for more customizable options
    runner.STYLESHEET_TMPL = &#39;<link rel="stylesheet" href="my_stylesheet.css" type="text/css">&#39;

    # run the test
    runner.run(my_test_suite)


------------------------------------------------------------------------
Copyright (c) 2004-2007, Wai Yip Tung
All rights reserved.

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:

* Redistributions of source code must retain the above copyright notice,
  this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
  notice, this list of conditions and the following disclaimer in the
  documentation and/or other materials provided with the distribution.
* Neither the name Wai Yip Tung nor the names of its contributors may be
  used to endorse or promote products derived from this software without
  specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER
OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
"""

# URL: http://tungwaiyip.info/software/HTMLTestRunner.html

author = "Wai Yip Tung"
version = "0.8.2"


"""
Change History

Version 0.8.2
* Show output inline instead of popup window (Viorel Lupu).

Version in 0.8.1
* Validated XHTML (Wolfgang Borgert).
* Added description of test classes and test cases.

Version in 0.8.0
* Define Template_mixin class for customization.
* Workaround a IE 6 bug that it does not treat <script> block as CDATA.

Version in 0.7.1
* Back port to Python 2.3 (Frank Horowitz).
* Fix missing scroll bars in detail log (Podi).
"""

# TODO: color stderr
# TODO: simplify javascript using ,ore than 1 class in the class attribute?
#coding=utf-8
import datetime
import io
import sys
reload(sys)
sys.setdefaultencoding(&#39;utf8&#39;)
import time
import unittest
import re
from xml.sax import saxutils


# ------------------------------------------------------------------------
# The redirectors below are used to capture output during testing. Output
# sent to sys.stdout and sys.stderr are automatically captured. However
# in some cases sys.stdout is already cached before HTMLTestRunner is
# invoked (e.g. calling logging.basicConfig). In order to capture those
# output, use the redirectors for the cached stream.
#
# e.g.
#   >>> logging.basicConfig(stream=HTMLTestRunner.stdout_redirector)
#   >>>

class OutputRedirector(object):
    """ Wrapper to redirect stdout or stderr """
    def init(self, fp):
        self.fp = fp

    def write(self, s):
        self.fp.write(s)

    def writelines(self, lines):
        self.fp.writelines(lines)

    def flush(self):
        self.fp.flush()

stdout_redirector = OutputRedirector(sys.stdout)
stderr_redirector = OutputRedirector(sys.stderr)



# ----------------------------------------------------------------------
# Template

class Template_mixin(object):
    """
    Define a HTML template for report customerization and generation.

    Overall structure of an HTML report

    HTML
    +------------------------+
    |<html>                  |
    |  <head>                |
    |                        |
    |   STYLESHEET           |
    |   +----------------+   |
    |   |                |   |
    |   +----------------+   |
    |                        |
    |  </head>               |
    |                        |
    |  <body>                |
    |                        |
    |   HEADING              |
    |   +----------------+   |
    |   |                |   |
    |   +----------------+   |
    |                        |
    |   REPORT               |
    |   +----------------+   |
    |   |                |   |
    |   +----------------+   |
    |                        |
    |   ENDING               |
    |   +----------------+   |
    |   |                |   |
    |   +----------------+   |
    |                        |
    |  </body>               |
    |</html>                 |
    +------------------------+
    """

    STATUS = {
    0: &#39;pass&#39;,
    1: &#39;fail&#39;,
    2: &#39;error&#39;,    
    }

    DEFAULT_TITLE = &#39;Unit Test Report&#39;
    DEFAULT_DESCRIPTION = &#39;&#39;

    # ------------------------------------------------------------------------
    # HTML Template

    HTML_TMPL = r"""<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
    <title>%(title)s</title>
    <meta name="generator" content="%(generator)s"/>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
    %(stylesheet)s
</head>
<body>
<script language="javascript" type="text/javascript"><!--
output_list = Array();

/* level - 0:Summary; 1:Failed; 2:All */
function showCase(level) {
    trs = document.getElementsByTagName("tr");
    for (var i = 0; i < trs.length; i++) {
        tr = trs[i];
        id = tr.id;
        if (id.substr(0,2) == &#39;ft&#39;) {
            if (level < 1) {
                tr.className = &#39;hiddenRow&#39;;
            }
            else {
                tr.className = &#39;&#39;;
            }
        }
        if (id.substr(0,2) == &#39;pt&#39;) {
            if (level > 1) {
                tr.className = &#39;&#39;;
            }
            else {
                tr.className = &#39;hiddenRow&#39;;
            }
        }

        }
    }
}

function showClassDetail(cid, count) {
    var id_list = Array(count);
    var toHide = 1;
    for (var i = 0; i < count; i++) {
        tid0 = &#39;t&#39; + cid.substr(1) + &#39;.&#39; + (i+1);
        tid = &#39;f&#39; + tid0;
        tr = document.getElementById(tid);
        if (!tr) {
            tid = &#39;p&#39; + tid0;
            tr = document.getElementById(tid);
        }        
        id_list[i] = tid;
        if (tr.className) {
            toHide = 0;
        }

    }
    for (var i = 0; i < count; i++) {
        tid = id_list[i];
        if (toHide) {
            document.getElementById(&#39;p_&#39;+tid).style.display = &#39;none&#39;
            document.getElementById(tid).className = &#39;hiddenRow&#39;;
        }
        else {
            document.getElementById(tid).className = &#39;&#39;;
        }
    }
}


function showTestDetail(p_id){
    var details_p = document.getElementById(p_id)
    var displayState = details_p.style.display
    // alert(displayState)
    if (displayState != &#39;block&#39; ) {
        displayState = &#39;block&#39;
        details_p.style.display = &#39;block&#39;
    }
    else {
        details_p.style.display = &#39;none&#39;
    }
}


function html_escape(s) {
    s = s.replace(/&/g,&#39;&&#39;);
    s = s.replace(/</g,&#39;<&#39;);
    s = s.replace(/>/g,&#39;>&#39;);
    return s;
}

/* obsoleted by detail in <p>
function showOutput(id, name) {
    var w = window.open("", //url
                    name,
                    "resizable,scrollbars,status,width=800,height=450");
    d = w.document;
    d.write("<pre class="brush:php;toolbar:false">");
    d.write(html_escape(output_list[id]));
    d.write("\n");
    d.write("<a href=&#39;javascript:window.close()&#39;>close</a>\n");
    d.write("
로그인 후 복사
\n"); d.close(); } */ --> %(heading)s %(report)s %(ending)s """ # variables: (title, generator, stylesheet, heading, report, ending) # ------------------------------------------------------------------------ # Stylesheet # # alternatively use a for external style sheet, e.g. # STYLESHEET_TMPL = """ """ # ------------------------------------------------------------------------ # Heading # HEADING_TMPL = """

%(title)s

%(parameters)s

%(description)s

""" # variables: (title, parameters, description) HEADING_ATTRIBUTE_TMPL = """

%(name)s: %(value)s

""" # variables: (name, value) # ------------------------------------------------------------------------ # Report # REPORT_TMPL = """

Show Summary Failed All

%(test_list)s
Test Group/Test case Count Pass Fail Error View
Total %(count)s %(Pass)s %(fail)s %(error)s
""" # variables: (test_list, count, Pass, fail, error) REPORT_CLASS_TMPL = r""" %(desc)s %(count)s %(Pass)s %(fail)s %(error)s Detail """ # variables: (style, desc, count, Pass, fail,skip, error, cid) """ # variables: (tid, Class, style, desc, status) """ # variables: (tid, Class, style, desc, status) REPORT_TEST_OUTPUT_TMPL = r""" %(id)s: %(output)s """ # variables: (id, output) # ------------------------------------------------------------------------ # ENDING # ENDING_TMPL = """

""" # -------------------- The end of the Template class ------------------- TestResult = unittest.TestResult class _TestResult(TestResult): # note: _TestResult is a pure representation of results. # It lacks the output and reporting ability compares to unittest._TextTestResult. def init(self, verbosity=1): TestResult.init(self) self.stdout0 = None self.stderr0 = None self.success_count = 0 self.failure_count = 0 self.error_count = 0 self.verbosity = verbosity # result is a list of result in 4 tuple # ( # result code (0: success; 1: fail; 2: error), # TestCase object, # Test output (byte string), # stack trace, # ) self.result = [] def startTest(self, test): TestResult.startTest(self, test) # just one buffer for both stdout and stderr self.outputBuffer = io.BytesIO() stdout_redirector.fp = self.outputBuffer stderr_redirector.fp = self.outputBuffer self.stdout0 = sys.stdout self.stderr0 = sys.stderr sys.stdout = stdout_redirector sys.stderr = stderr_redirector def complete_output(self): """ Disconnect output redirection and return buffer. Safe to call multiple times. """ if self.stdout0: sys.stdout = self.stdout0 sys.stderr = self.stderr0 self.stdout0 = None self.stderr0 = None return self.outputBuffer.getvalue() def stopTest(self, test): # Usually one of addSuccess, addError or addFailure would have been called. # But there are some path in unittest that would bypass this. # We must disconnect stdout in stopTest(), which is guaranteed to be called. self.complete_output() def addSuccess(self, test): self.success_count += 1 TestResult.addSuccess(self, test) output = self.complete_output() self.result.append((0, test, output, '')) if self.verbosity > 1: sys.stderr.write('ok ') sys.stderr.write(str(test)) sys.stderr.write('\n') else: sys.stderr.write('.') def addError(self, test, err): self.error_count += 1 TestResult.addError(self, test, err) _, _exc_str = self.errors[-1] output = self.complete_output() self.result.append((2, test, output, _exc_str)) if self.verbosity > 1: sys.stderr.write('E ') sys.stderr.write(str(test)) sys.stderr.write('\n') else: sys.stderr.write('E') def addFailure(self, test, err): self.failure_count += 1 TestResult.addFailure(self, test, err) _, _exc_str = self.failures[-1] output = self.complete_output() self.result.append((1, test, output, _exc_str)) if self.verbosity > 1: sys.stderr.write('F ') sys.stderr.write(str(test)) sys.stderr.write('\n') else: sys.stderr.write('F') class HTMLTestRunner(Template_mixin): """ """ def init(self, stream=sys.stdout, verbosity=1, title=None, description=None,name=None): self.stream = stream self.verbosity = verbosity if title is None: self.title = self.DEFAULT_TITLE else: self.title = title if name is None: self.name ='' else: self.name = name if description is None: self.description = self.DEFAULT_DESCRIPTION else: self.description = description self.startTime = datetime.datetime.now() def run(self, test): "Run the given test case or test suite." result = _TestResult(self.verbosity) test(result) self.stopTime = datetime.datetime.now() self.generateReport(test, result) # print (sys.stderr, '\nTime Elapsed: %s' % (self.stopTime-self.startTime)) return result def sortResult(self, result_list): # unittest does not seems to run in any particular order. # Here at least we want to group them together by class. rmap = {} classes = [] for n,t,o,e in result_list: cls = t.class if not cls in rmap: rmap[cls] = [] classes.append(cls) rmap[cls].append((n,t,o,e)) r = [(cls, rmap[cls]) for cls in classes] return r def getReportAttributes(self, result): """ Return report attributes as a list of (name, value). Override this to add custom attributes. """ startTime = str(self.startTime)[:19] duration = str(self.stopTime - self.startTime) status = [] if result.success_count: status.append('Pass %s' % result.success_count) if result.failure_count: status.append('Failure %s' % result.failure_count) if result.error_count: status.append('Error %s' % result.error_count ) if status: status = ' '.join(status) else: status = 'none' return [ ('Start Time', startTime), ('Duration', duration), ('Status', status), ] def generateReport(self, test, result): report_attrs = self.getReportAttributes(result)#报告的头部 generator = 'HTMLTestRunner %s' % version stylesheet = self._generate_stylesheet()#拿到css文件 heading = self._generate_heading(report_attrs) report = self._generate_report(result) ending = self._generate_ending() output = self.HTML_TMPL % dict( title = saxutils.escape(self.title), generator = generator, stylesheet = stylesheet, heading = heading, report = report, ending = ending, ) self.stream.write(output.encode('utf8')) def _generate_stylesheet(self): return self.STYLESHEET_TMPL def _generate_heading(self, report_attrs): a_lines = [] for name, value in report_attrs: line = self.HEADING_ATTRIBUTE_TMPL % dict( name = saxutils.escape(name), value = saxutils.escape(value), ) a_lines.append(line) heading = self.HEADING_TMPL % dict( title = saxutils.escape(self.title), parameters = ''.join(a_lines), description = saxutils.escape(self.description), ) return heading #根据result收集报告 def _generate_report(self, result): rows = [] sortedResult = self.sortResult(result.result) i = 0 for cid, (cls, cls_results) in enumerate(sortedResult): # subtotal for a class for n,t,o,e in cls_results: if n == 0: np += 1 elif n == 1: nf += 1 else: ne += 1 # format class description # if cls.module == "main": # name = cls.name # else: # name = "%s.%s" % (cls.module, cls.name) name = cls.name try: core_name=self.name[i] except Exception,e: core_name ='' # doc = (cls.doc)+core_name and (cls.doc+core_name).split("\n")[0] or "" doc = (cls.doc) and cls.doc .split("\n")[0] or "" desc = doc and '%s: %s' % (name, doc) or name i=i+1 #生成每个TestCase类的汇总数据,对于报告中的 row = self.REPORT_CLASS_TMPL % dict( style = ne > 0 and 'errorClass' or nf > 0 and 'failClass' or 'passClass', desc = desc, , Pass = np, fail = nf, error = ne, cid = 'c%s' % (cid+1), ) rows.append(row) #生成每个TestCase类中所有方法的测试结果 for tid, (n,t,o,e) in enumerate(cls_results): self._generate_report_test(rows, cid, tid, n, t, o, e) report = self.REPORT_TMPL % dict( test_list = ''.join(rows), count = str(result.success_count+result.failure_count+result.error_count+result.skipped_count), Pass = str(result.success_count), fail = str(result.failure_count), error = str(result.error_count), ) return report def _generate_report_test(self, rows, cid, tid, n, t, o, e): # e.g. 'pt1.1', 'ft1.1', etc has_output = bool(o or e) name = t.id().split('.')[-1] doc = t.shortDescription() or "" desc = doc and ('%s: %s' % (name, doc)) or name tmpl = has_output and self.REPORT_TEST_WITH_OUTPUT_TMPL or self.REPORT_TEST_NO_OUTPUT_TMPL uo1="" # o and e should be byte string because they are collected from stdout and stderr? if isinstance(o,str): uo = str(o) else: uo = e if isinstance(e,str): # TODO: some problem with 'string_escape': it escape \n and mess up formating # ue = unicode(e.encode('string_escape')) ue = e else: ue = o script = self.REPORT_TEST_OUTPUT_TMPL % dict( id = tid, output = saxutils.escape(str(uo) + str(ue)) ) row = tmpl % dict( tid = tid, Class = (n == 0 and 'hiddenRow' or 'none'), desc = desc, script = script, hidde=hidde_status, image=image_url, status = self.STATUS[n], ) rows.append(row) if not has_output: return def _generate_ending(self): return self.ENDING_TMPL ############################################################################## # Facilities for running tests from the command line ############################################################################## # Note: Reuse unittest.TestProgram to launch test. In the future we may # build our own launcher to support more specific command line # parameters like test title, CSS, etc. # class TestProgram(unittest.TestProgram): # """ # A variation of the unittest.TestProgram. Please refer to the base # class for command line parameters. # """ # def runTests(self): # # Pick HTMLTestRunner as the default test runner. # # base class's testRunner parameter is not useful because it means # # we have to instantiate HTMLTestRunner before we know self.verbosity. # if self.testRunner is None: # self.testRunner = HTMLTestRunner(verbosity=self.verbosity) # unittest.TestProgram.runTests(self) # # main = TestProgram ############################################################################## # Executing this module from the command line ############################################################################## if name == "main": main(module=None)

把上面代码复制覆盖原来的HTMLTestRunner就好,截图那块我是把错误的图像放在apache服务器的某个路径下的,如果有错误就显示图片超链接,没有就隐藏这超链接。

关于上面的改动其实很简单,熟悉一定的前端语言(html.javascript)即可。HTMLTestRunner原理就是我们上文提到的利用_TestResult继承unnitest中的TestResult类,并重写了addSuccess,addSkip,addError等方法,把测试结果放在一个self.result里面,最后遍历这个result利用前端的一些知识生成一个html报告。这边贴图贴一下生成的样式吧,执行testcase的代码:

#coding=utf-8
import unittest
import HTMLTestRunner
import sys,os
class Mydemo(unittest.TestCase):
    def test1(self):
        print "excute test1"
    @unittest.Myskip
    def test2(self):
        print "excute test2"
        raise AssertionError("test2 fail")
    @unittest.Myskip
    def test3(self):
        print "excute test3"
    @unittest.Myskip
    def test4(self):
        print "excute test4"
if name == &#39;main&#39;:
    module_name=os.path.basename(sys.argv[0]).split(".")[0]
    module=import(module_name)
    fp=file("./new.html","wb")
    runner=HTMLTestRunner.HTMLTestRunner(fp)
    all_suite=unittest.defaultTestLoader.loadTestsFromModule(module)
    runner.run(all_suite)
로그인 후 복사

最后生成的报告如下:

Python의 단위 테스트 및 확장된 HTMLTestRunner에 대한 자세한 설명

 

위 내용은 Python의 단위 테스트 및 확장된 HTMLTestRunner에 대한 자세한 설명의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!

원천:php.cn
본 웹사이트의 성명
본 글의 내용은 네티즌들의 자발적인 기여로 작성되었으며, 저작권은 원저작자에게 있습니다. 본 사이트는 이에 상응하는 법적 책임을 지지 않습니다. 표절이나 침해가 의심되는 콘텐츠를 발견한 경우 admin@php.cn으로 문의하세요.
인기 튜토리얼
더>
최신 다운로드
더>
웹 효과
웹사이트 소스 코드
웹사이트 자료
프론트엔드 템플릿