unittest原理:https://www.jianshu.com/p/c3fd61ac09e9
因為使用unittest進行管理case的運行。有時case因為偶然因素,會隨機的失敗。通過重試機制能夠補充保持case的穩定性。查閱資料后發現,python的unittest自身無失敗重試機制,可以通過以下手段達到目的:1.修改unittest源碼,使test case重新運行若干次 2. 對case結果進行處理,單獨調度運行失敗的case。此篇我們來了解下如何通過修改源碼進行失敗重試。使用的python版本為2.7。
* 百度上搜索失敗重試,找到這個文章。http://blog.csdn.net/hqzxsc2006/article/details/50349664。我嘗試了此方法,並對其中部分邏輯進行了修改。后來發現我需要的運行場景中此方法無法滿足要求,隨放棄了該方法。
1. 調用unittest運行case
def TestSuite(filedirname):
loader = unittest.TestLoader()
dir_case = loader.discover(start_dir=config.case_dir, pattern=filedirname, top_level_dir=None)
return dir_case
if __name__ == '__main__':
filedirname = 'Test*' # 需要執行的case
unittest.TextTestRunner(verbosity=2).run(TestSuite(filedirname))
2. 定位運行case的源碼1
以上為調用unittest的方法。通過loader.discover掃描指定dir中所有以Test打頭的文件,加載所有的case。然后通過unittest.TextTestRunner(verbosity=2).run()方法來運行所有的case。進入 TextTestResult類查看run方法,理解代碼后發現:先初始化運行結果result,然后初始化startTestRun,運行test(result)后,處理result里記錄的結果。通過加入打印等方式發現,此處的test(result),將加載的所有case都運行了。所以無法在此處做失敗重試。
3. 定位運行case的源碼2
通過函數定義可看出:TextTestResult.run(self, test)中的test即為:TestSuite(filedirname)函數的返回值。查看TestLoader.discover()函數,發現返回類型為:TestSuite。此類的run方法,即為搜索到的文章所修改的代碼處。源碼不支持中文,注釋中有中文也不行,否則運行case將報錯。以下為了寫文章方便,使用中文注釋。
class TestSuite(BaseTestSuite):
def run(self, result, debug=False):
failcount = 1
class_num = 1
topLevel = False
if getattr(result, '_testRunEntered', False) is False:
result._testRunEntered = topLevel = True
for test in self:
case_num = 1
if result.shouldStop:
break
success_flag = True
while success_flag:
if _isnotsuite(test):
self._tearDownPreviousClass(test, result)
self._handleModuleFixture(test, result)
self._handleClassSetUp(test, result)
result._previousTestClass = test.__class__
if (getattr(test.__class__, '_classSetupFailed', False) or
getattr(result, '_moduleSetUpFailed', False)):
if class_num > failcount:
success_flag = False
else:
time.sleep(5)
result._previousTestClass = None
print 'Class %s retry time(s) %s'%(test.__class__,class_num)
class_num += 1
continue
f,e = map(len, (result.failures, result.errors)) #查看result類得知失敗和錯誤的保存在此
cntBefore = f + e
if not debug:
test(result)
else:
test.debug()
f,e = map(len, (result.failures, result.errors))
cntAfter = f + e
if cntAfter > cntBefore:
if case_num > failcount:
success_flag = False
else:
print 'Test % retry time(s): %s'%(test,case_num)
case_num += 1
else:
success_flag = False
if topLevel:
self._tearDownPreviousClass(None, result)
self._handleModuleTearDown(result)
result._testRunEntered = False
return result
實驗中,通過以下方法調用,使用ok:
def suit():
s = unittest.TestSuite()
s.addTest(Test_map("test_xxx1"))
s.addTest(Test_map("test_xxx2"))
unittest.TextTestRunner().run(s)
if __name__ == '__main__':
suit()
當我以為問題解決時,發現通過1中的unittest.TextTestRunner(verbosity=2).run(TestSuite(filedirname))執行時,即行不通。當case失敗時,就一直重試,進入死循環。debug時,發現當return result后,代碼再一次進入了test(result),並且case_num=1,success_flag=True。所以進入循環,無法出來了。為何return后又進入test(result)了呢,我沒理解出來....
3. 定位運行case源代碼3
繼續尋找合適的位置修改代碼。debug時發現,TestSuite.run中的test(result)方法,實際上運行的是case.py中的TestCase.run方法。在此處修改也應可以實現重試。run中的其他代碼保持不變。
retryCnt = 0
retryMax = 3
success = False
while (not success) and (retryCnt < retryMax): #此處加入一個循環
print "RetryCnt:", retryCnt
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)
else: #此次為新加入的代碼
retryCnt += 1
print "----run case failed---"
可以看到,基本上只是加入了一個循環和else分支。運行后,得到了與預期基本一致的結果。case A若失敗,會重新運行,直到成功。每個case最多運行retryMax次。結果中記錄每次運行的結果,如case A運行失敗1次,成功1次。
