目录
前言
unittest.TestResult类简介
TestResult类定制目标
实现步骤
测试结果summary格式规划
单个用例结果格式规划
根据测试方法对象获取用例代码
单个用例结果类的实现
TestResult属性及初始化方法
测试开始和测试结束
用例开始和用例结束
1. 重写恢复输出流方法
2. 用例开始和结束方法
用例结果注册
测试本TestResult类方法
其他函数和方法
1. 用例状态列
2. 获取平台信息
3. 从异常中提取异常信息方法
4. 从异常和已知异常中提取失败原因的方法
前言
Unittest是Python自带的自动化测试框架,提供了基本的控制结构和模型概念。
由于Unittest功能较为基础,因此在实际框架实战中往往需要对其功能进行扩充。
比如:
·生成HTML报告
·多线程并发(并且报告不混乱)
·自动重试出错用例
·为用例提供tags标签和level等级等,往往需要我们对Unittest框架进行二次开发和扩展,由于Unittest框架清晰的API,扩展和定制也非常方便。
unittest.TestResult类简介
TestResult类一般在TestRunner类中实例化,并穿梭于每个执行的测试套件和测试用例中用于记录结果。
TestResult对象常用的属性有:
·stream:用于输出测试信息的IO流,一般是终端或文本文件。
·descriptions:描述信息。
·verbosity:显示详细级别。
·buffer:默认为False,用例中的print信息立即输出,buffer为True时将用例中的print信息统一收集并集中输出。
·tb_locals: 在报错异常信息中显示用例中的局部变量(即tackback_locals)。
·failfast:默认为False, 用例失败后继续运行,为True时,任何一条用例失败时立即停止。
·_mirrorOutput:是否重定向输出流状态标志unittest.TestResult类提供了以下几种方法:
-运行开始/结束
startTestRun: 执行开始时调用,参考unittest.TextTestRunner中的run方法。
stopTestRun: 所有用例执行结束后调用
startTest:单个用例执行开始时调用,参考unittest.TestCase类中的run方法。
stopTest:单个用例执行结束后调用。
-注册用例结果
addSuccess:单个用例执行成功时调用,来注册结果,默认为空。
addFailure:用例失败时在stopTest前调用。
addError:用例异常时在stopTest前调用。
addSkip:用例跳过时在stopTest前调用。
addExpectedFailure:用例期望失败时在stopTest前调用。
addUnexpectedSuccess:用例非期望成功时在stopTest前调用。
-重定向和恢复系统输出流
_setupStdout:重定向输出流,默认self.buffer为True时生效
_restoreStdout:恢复系统输出流
用例失败Failure和用例异常Error的区别:
用例中的断言错误(期望结果和实际结果不一致)引发的AssertionError异常被视为用例失败,其他异常视为用例异常Error。
ExpectedFailure和UnexpectedSuccess: 期望失败指我们期望这条用例执行失败,即用例失败了才是符合预期的,而没有失败即UnexpectedSuccess,这是一种反向用例,如果失败了其实是通过,而成功了反而是失败。
TestResult类定制目标
1. 在result中增加整体的运行开始时间start_at,持续时间duration和每条用例的开始时间,执行时间
2. 存储用例中的print信息及异常信息,以供生成HTML使用
3. 为已知异常提供失败原因
4. 提供结构化和可序列化的summary和详情数据
5. 探测每个用例code,以为审视用例代码提供方便
6. 增加运行平台platform信息和运行时的环境变量信息
7. 将print信息改为使用log记录,增加日志时间,方便追溯。
8. 提供用例的更多的信息,如tags,level, id, 描述等信息。
实现步骤
测试结果summary格式规划
测试结果result类提供一个summary属性,格式如下(参考了httprunner的summary格式):
name: result结果名称success: 整个测试结果是否成功stat: # 结果统计信息testsRun: 总运行数successes: 成功数failures: 失败数errors: 异常数skipped: 跳过的用例数expectedFailures: 期望失败数unexpectedSuccesses: 非期望成功数time:start_at: 整个测试开始时间(时间戳)end_at: 增高测试结束时间(时间戳)duration: 整个测试执行耗时(秒)platform:platform: 执行平台信息system: 执行操作系统信息python_version: Python版本信息# env: 环境变量信息(信息中可能包含账号等敏感信息)details: # 用例结果详情- ... # 单个用例结果
单个用例结果格式规划
# 执行前可获取的信息name: 用例名称或用例方法名id: 用例完整路径(模块-类-用例方法名)decritpion: 用例描述(用例方法docstring第一行)doc: 用例方法完整docstringmodule_name: 用例模块名class_name: 用例类名class_id: 用例类路径(模块-类)class_doc: 用例类docstring描述tags: 用例标签level: 用例等级code: 用例代码# 执行后可获取的信息time:start_at: 用例执行开始时间end_at: 用例结束时间duration: 用例执行持续时间status: 用例执行状态success/fail/error/skipped/xfail/xpassoutput: 用例中的print输出信息exc_info: 用例异常追溯信息reason: 用例跳过,失败,出错的原因
读者也可以根据自己的需要添加其他额外的信息,如timeout用例超时时间配置,order用例执行顺序,images用例中的截图,link用例中的链接等信息。
以上的tags和level通过在用例方法的docstring中注入"tag:smoke"及"level:1"等样式为用例添加标签和等级,然后配合定制的loader用例加载器去收集指定标签或等级的用例,下节会详细讲解。
用例tags和level的实现
每个框架都会有自己约定格式,这里我采用在docstring注入特定格式描述的方式为用例添加tags和level信息,用例格式如下。
import unittestclass TestDemo(unittest.TestCase):def test_a(self):"""测试atag:smoketag:demolevel:1"""print('测试a')
对于每个用例对象,可以使用test._testMethodDoc来获取其完整的docstring字符串,然后通过正则匹配来匹配出用例的tags列表和level等级,实现方法如下。
import reTAG_PARTTEN = 'tag:(\w+)'LEVEL_PARTTEN = 'level:(\d+)'def get_case_tags(case: unittest.TestCase) -> list:"""从用例方法的docstring中匹配出指定格式的tags"""case_tags = Nonecase_doc = case._testMethodDocif case_doc and 'tag' in case_doc:pattern = re.compile(TAG_PARTTEN)case_tags = re.findall(pattern, case_doc)return case_tagsdef get_case_level(case: unittest.TestCase):"""从用例方法的docstring中匹配出指定格式的level"""case_doc = case._testMethodDoccase_level = None # todo 默认levelif case_doc:pattern = re.compile(LEVEL_PARTTEN)levels = re.findall(pattern, case_doc)if levels:case_level = levels[0]try:case_level = int(case_level)except:raise ValueError(f'用例中level设置:{case_level} 应为整数格式')return case_level
根据测试方法对象获取用例代码
def inspect_code(test):test_method = getattr(test.__class__, test._testMethodName)try:code = inspect.getsource(test_method)except Exception as ex:log.exception(ex)code = ''return code
单个用例结果类的实现
由于单个用例结果信息较多,我们可以在整个TestResult类中使用一个嵌套字典格式存储,也可以单独定制一个用例结果类,参考如下。
class TestCaseResult(object):"""用例测试结果"""def __init__(self, test: unittest.case.TestCase, name=None): self.test = test # 测试用例对象self.name = name or test._testMethodName # 支持传入用例别名,unittest.TestCase自带属性方法self.id = test.id() # 用例完整路径,unittest.TestCase自带方法self.description = test.shortDescription() # 用例简要描述,unittest.TestCase自带方法self.doc = test._testMethodDoc # 用例docstring,,unittest.TestCase自带属性方法self.module_name = test.__module__ # 用例所在模块名self.class_name = test.__class__.__name__ # 用例所在类名self.class_id = f'{test.__module__}.{test.__class__.__name__}' # 用例所在类完整路径self.class_doc = test.__class__.__doc__ # 用例所在类docstring描述self.tags = get_case_tags(test) # 获取用例tagsself.level = get_case_level(test) # 获取用例level等级self.code = inspect_code(test) # 获取用例源代码# 用例执后更新的信息self.start_at = None # 用例开始时间self.end_at = None # 用例结束时间self.duration = None # 用例执行持续时间self.status = None # 用例测试状态self.output = None # 用例内的print信息self.exc_info = None # 用例异常信息self.reason = None # 跳过,失败,出错原因@propertydef data(self): # 组合字典格式的用例结果数据data = dict(name=self.name,id=self.id,description=self.description,status=self.status,tags=self.tags,level=self.level,time=dict( # 聚合时间信息start_at=self.start_at,end_at=self.end_at,duration=self.duration),class_name=self.class_name,class_doc=self.class_doc,module_name=self.module_name,code=self.code,output=self.output,exc_info=self.exc_info,reason=self.reason,)return data
TestResult属性及初始化方法
根据上面对测试结果summary格式的规划,我们继承unittest.TestResult类来定制我们的测试结果类。
import unittestclass TestResult(unittest.TestResult):"""定制的测试结果类,补充用例运行时间等更多的执行信息"""def __init__(self,stream=None,descriptions=None,verbosity=None):super().__init__(stream, descriptions, verbosity) # 调用父类方法,继承父类的初始化属性,然后再进行扩充# 对父类的默认熟悉做部分修改self.testcase_results = [] # 所有用例测试结果对象(TestCaseResult对象)列表self.successes = [] # 成功用例对象列表,万一用得着呢self.verbosity = verbosity or 1 # 设置默认verbosity为1self.buffer = True # 在本定制方法中强制使用self.buffer=True,缓存用例输出self.name = None # 提供通过修改result对象的name属性为结果提供名称描述 self.start_at = Noneself.end_at = Noneself.duration = None# 由于继承的父类属性中存在failures、errors等属性(存放失败和异常的用例列表),此处加以区分self.successes_count = 0 # 成功用例数self.failures_count = 0 # 失败用例数self.errors_count = 0 # 异常用例数self.skipped_count = 0 # 跳过用例数self.expectedFailures_count = 0 # 期望失败用例数self.unexpectedSuccesses_count = 0 # 非期望成功用例数self.know_exceptions = {} # 已知异常字典,用于通过异常名来映射失败原因,如# self.know_exceptions = {'requests.exceptions.ConnectionError': '请求连接异常'}@propertydef summary(self):"""组装结果概要, details分按运行顺序和按类组织两种结构"""data = dict(name=self.name,success=self.wasSuccessful(), # 用例是否成功,父类unittest.TestResult自带方法stat=dict(testsRun=self.testsRun,successes=self.successes_count,failures=self.failures_count,errors=self.errors_count,skipped=self.skipped_count,expectedFailures=self.expectedFailures_count,unexpectedSuccesses=self.unexpectedSuccesses_count,),time=dict(start_at=self.start_at,end_at=self.end_at,duration=self.duration),platform=get_platform_info(),details=[item.data for item in self.testcase_results] # 每个测试用例结果对象转为其字典格式的数据)return data
测试开始和测试结束
使用log信息代替原来的print输出到stream流,这里使用的是笔者发布的开源包logz,安装方法为:
pip install logz
logz非常方便配置和使用,支持方便的配置,单例,DayRoting,准确的调用追溯以及log到Email等,详细使用方法可参考:https://github.com/hanzhichao/logz。
TestResult类中的verbosity属性用于控制输出信息的详细等级,unittest.TextTestResult分为0,1,2三级,作者这里也采用3级模式,逻辑稍有不同,这里设计的逻辑如下。
1、verbosity>1时:输出整个执行开始和结束信息,每个用例除自身print输出外,打印两条开始和结束两条日志,分别显示用例名称描述+执行时间和执行结果+持续时间。
2、verbosity为1时:不输出整体开始和结束信息,只每天用例输出用例方法名和执行状态一行日志。
3、verbosity为0时:不输出任何信息,包括错误信息。
以下为对父类执行开始和执行结束方法的重写。
import timefrom logz import log # 需要安装logzdef time_to_string(timestamp: float) -> str:"""时间戳转时间字符串,便于日志中更易读""time_array = time.localtime(timestamp)time_str = time.strftime("%Y-%m-%d %H:%M:%S", time_array)return time_strclass TestResut(unittest.TestResult):...def startTestRun(self):"""整个执行开始"""self.start_at = time.time() # 整个执行的开始时间if self.verbosity > 1:self._log(f'===== 测试开始, 开始时间: {time_to_string(self.start_at)} =====')def stopTestRun(self):"""整个执行结束"""self.end_at = time.time() # 整个执行的结束时间self.duration = self.end_at - self.start_at # 整个执行的持续self.success = self.wasSuccessful() # 整个执行是否成功if self.verbosity > 1:self._log(f'===== 测试结束, 持续时间: {self.duration}秒 =====')
由于父类中的startTestRun和stopTestRun没有任何内容,此处不需要再调用父类的方法。
原始的unittest.TextTestRunner中对整个执行时间的统计是在result对象外的,此处集成到result对象中,已使result的结果信息更完整。
用例开始和用例结束
捕获用例输出信息,在用例中常常会有print信息或出错信息,这里面的信息是直接写到系统标准输出stdout和stderr中的。要捕获并记录这些信息的话,我们需要再执行用例的过程中(从startTest到stopTest)将系统stdout和stderr临时重定向到我们的io流变量中,然后通过get_value()获取其中的字符串。
可喜的是,父类unittest.TestResult中便提供了重定向和恢复输出的参考方法,我们稍微改动即可。
1. 重写恢复输出流方法
由于startTest父类中自动调用_setupOutput方法,并且强制self.buffer为True,因此会自动重定向信息流,无需重写。
这里去掉了对原始输出流的信息输出,改为return字符串,之后再使用log输出。
def _restoreStdout(self):"""重写父类的_restoreStdout方法并返回output+error"""if self.buffer:output = error = ''if self._mirrorOutput:output = sys.stdout.getvalue()error = sys.stderr.getvalue()# 去掉了对原始输出流的信息输出sys.stdout = self._original_stdoutsys.stderr = self._original_stderrself._stdout_buffer.seek(0)self._stdout_buffer.truncate()self._stderr_buffer.seek(0)self._stderr_buffer.truncate()return output + error or None # 改为return字符串,之后再log输出
2. 用例开始和结束方法
def startTest(self, test: unittest.case.TestCase):"""单个用例执行开始"""super().startTest(test) # 调用父类方法test.result = TestCaseResult(test) # 实例化用例结果对象来记录用例结果,并绑定用例的result属性self.testcase_results.append(test.result) # 另外添加到所有的结果列表一份test.result.start_at = time.time() # 记录用例开始时间if self.verbosity > 1:self._log(f'执行用例: {test.result.name}: {test.result.description}, 开始时间: {time_to_string(test.result.start_at)}')def stopTest(self, test: unittest.case.TestCase) -> None:"""单个用例结束"""test.result.end_at = time.time() # 记录用例结束时间test.result.duration = test.result.end_at - test.result.start_at # 记录用例持续时间# 由于output要从_restoreStdout获取,手动加入父类恢复输出流的方法test.result.output = self._restoreStdout()self._mirrorOutput = False # 是否重定向输出流标志
用例结果注册
def addSuccess(self, test):"""重写父类方法, 单个用例成功时在stopTest前调用"""test.result.status = TestStatus.SUCCESSself.successes.append(test)self.successes_count += 1super().addSuccess(test)@failfastdef addFailure(self, test, err):"""重写父类方法, 用例失败时在stopTest前调用"""test.result.status = TestStatus.FAILtest.result.exc_info = self._exc_info_to_string(err, test)test.result.reason = self._get_exc_msg(err)self.failures_count += 1super().addFailure(test, err)@failfastdef addError(self, test, err):"""重写父类方法, 用例异常时在stopTest前调用"""test.result.status = TestStatus.ERRORtest.result.exc_info = self._exc_info_to_string(err, test)test.result.reason = self._get_exc_msg(err)self.errors_count += 1super().addError(test, err)def addSkip(self, test, reason):"""重写父类方法, 用例跳过时在stopTest前调用"""test.result.status = TestStatus.SKIPPEDtest.result.reason = reasonself.skipped_count += 1super().addSkip(test, reason)def addExpectedFailure(self, test, err):"""重写父类方法, 用例期望失败时在stopTest前调用"""test.result.status = TestStatus.XFAILtest.result.exc_info = self._exc_info_to_string(err, test)test.result.reason = self._get_exc_msg(err)self.expectedFailures_count += 1super().addExpectedFailure(test, err)@failfastdef addUnexpectedSuccess(self, test):"""重写父类方法, 用例非期望成功时在stopTest前调用"""test.result.status = TestStatus.XPASSself.expectedFailures_count += 1super().addUnexpectedSuccess(test)
测试本TestResult类方法
if __name__ == '__main__':import unittestclass TestDemo(unittest.TestCase): def test_a(self): # 可以添加更多的用例进行测试"""测试atag:smoketag:demolevel:1"""print('测试a')suite = unittest.defaultTestLoader.loadTestsFromTestCase(TestDemo)runner = unittest.TextTestRunner(resultclass=TestResult) # 使用定制的TestResult类result = runner.run(suite)print(result.summary) # 输出result的字典格式数据,建议使用pprint输出,需要安装pprint
注:由于和作者本人自己使用的TestResult类有所精简和改动,尚未进行更多的测试,如有问题欢迎留言指正。
其他函数和方法
1. 用例状态列
为了方便修改状态名称,(如改成中文),这里使用用例状态类。
class TestStatus(object):SUCCESS = 'success'FAIL = 'fail'ERROR = 'error'SKIPPED = 'skipped'XFAIL = 'xfail'XPASS = 'xpass'
2. 获取平台信息
import osdef get_platform_info():"""获取执行平台信息"""return {"platform": platform.platform(),"system": platform.system(),"python_version": platform.python_version(),# "env": dict(os.environ),}
3. 从异常中提取异常信息方法
def _exc_info_to_string(self, err, test):"""重写父类的转换异常方法, 去掉buffer的输出"""exctype, value, tb = errwhile tb and self._is_relevant_tb_level(tb):tb = tb.tb_nextif exctype is test.failureException:# Skip assert*() traceback levelslength = self._count_relevant_tb_levels(tb)else:length = Nonetb_e = traceback.TracebackException(exctype, value, tb, limit=length, capture_locals=self.tb_locals)msgLines = list(tb_e.format())return ''.join(msgLines)
4. 从异常和已知异常中提取失败原因的方法
def _get_exc_msg(self, err):exctype, value, tb = errexc_msg = str(value)exc_full_path = f'{exctype.__module__}.{exctype.__name__}'if self.know_exceptions and isinstance(self.know_exceptions, dict):exc_msg = self.know_exceptions.get(exc_full_path, exc_msg)return exc_msg
作为一位过来人也是希望大家少走一些弯路,在这里我给大家分享一些自动化测试前进之路的必须品,希望能对你带来帮助。(WEB自动化测试、app自动化测试、接口自动化测试、持续集成、自动化测试开发、大厂面试真题、简历模板等等),相信能使你更好的进步!
留【自动化测试】即可【自动化测试交流】:574737577(备注ccc)http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=hIqEQD5B5ZyLT0S-vFq64p5MCDBc8jJU&authKey=O%2B3T95fjNUNsYxXnPIrOxvkb%2BbuFd1AxuUP5gCbos34AQDjaRG2L6%2Fm9gGakvo94&noverify=0&group_code=574737577