Unittest二次开发实战

目录

  前言

  unittest.TestResult类简介

  TestResult类定制目标

  实现步骤

  测试结果summary格式规划

  单个用例结果格式规划

  用例tags和level的实现

  根据测试方法对象获取用例代码

  单个用例结果类的实现

  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: 用例方法完整docstring
  module_name: 用例模块名
  class_name: 用例类名
  class_id: 用例类路径(模块-类)
  class_doc: 用例类docstring描述
  tags: 用例标签
  level: 用例等级
  code: 用例代码
  # 执行后可获取的信息
  time:
    start_at: 用例执行开始时间
    end_at: 用例结束时间
    duration: 用例执行持续时间
  status: 用例执行状态success/fail/error/skipped/xfail/xpass
  output: 用例中的print输出信息
  exc_info: 用例异常追溯信息
  reason: 用例跳过,失败,出错的原因

  读者也可以根据自己的需要添加其他额外的信息,如timeout用例超时时间配置,order用例执行顺序,images用例中的截图,link用例中的链接等信息。

  以上的tags和level通过在用例方法的docstring中注入"tag:smoke"及"level:1"等样式为用例添加标签和等级,然后配合定制的loader用例加载器去收集指定标签或等级的用例,下节会详细讲解。

  用例tags和level的实现

  每个框架都会有自己约定格式,这里我采用在docstring注入特定格式描述的方式为用例添加tags和level信息,用例格式如下。

import unittest
  class TestDemo(unittest.TestCase):
      def test_a(self):
          """测试a
          tag:smoke
          tag:demo
          level:1
          """
          print('测试a')

  对于每个用例对象,可以使用test._testMethodDoc来获取其完整的docstring字符串,然后通过正则匹配来匹配出用例的tags列表和level等级,实现方法如下。

import re
  TAG_PARTTEN = 'tag:(\w+)'
  LEVEL_PARTTEN = 'level:(\d+)'
  def get_case_tags(case: unittest.TestCase) -> list:
      """从用例方法的docstring中匹配出指定格式的tags"""
      case_tags = None
      case_doc = case._testMethodDoc
      if case_doc and 'tag' in case_doc:
          pattern = re.compile(TAG_PARTTEN)
          case_tags = re.findall(pattern, case_doc)
      return case_tags
  def get_case_level(case: unittest.TestCase):
      """从用例方法的docstring中匹配出指定格式的level"""
      case_doc = case._testMethodDoc
      case_level = None  # todo 默认level
      if 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)   # 获取用例tags
          self.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  # 跳过,失败,出错原因
      @property
      def 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 unittest
  class 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为1
           self.buffer = True  # 在本定制方法中强制使用self.buffer=True,缓存用例输出
          
          self.name = None  # 提供通过修改result对象的name属性为结果提供名称描述 
          self.start_at = None
          self.end_at = None
          self.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': '请求连接异常'}
          @property
          def 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 time
  from logz import log  # 需要安装logz
  def 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_str
  class 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_stdout
              sys.stderr = self._original_stderr
              self._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.SUCCESS
          self.successes.append(test)
          self.successes_count += 1
          super().addSuccess(test)
      @failfast
      def addFailure(self, test, err):
          """重写父类方法, 用例失败时在stopTest前调用"""
          test.result.status = TestStatus.FAIL
          test.result.exc_info = self._exc_info_to_string(err, test)
          test.result.reason = self._get_exc_msg(err)
          self.failures_count += 1
          super().addFailure(test, err)
      @failfast
      def addError(self, test, err):
          """重写父类方法, 用例异常时在stopTest前调用"""
          test.result.status = TestStatus.ERROR
          test.result.exc_info = self._exc_info_to_string(err, test)
          test.result.reason = self._get_exc_msg(err)
          self.errors_count += 1
          super().addError(test, err)
      def addSkip(self, test, reason):
          """重写父类方法, 用例跳过时在stopTest前调用"""
          test.result.status = TestStatus.SKIPPED
          test.result.reason = reason
          self.skipped_count += 1
          super().addSkip(test, reason)
      def addExpectedFailure(self, test, err):
          """重写父类方法, 用例期望失败时在stopTest前调用"""
          test.result.status = TestStatus.XFAIL
          test.result.exc_info = self._exc_info_to_string(err, test)
          test.result.reason = self._get_exc_msg(err)
          self.expectedFailures_count += 1
          super().addExpectedFailure(test, err)
      @failfast
      def addUnexpectedSuccess(self, test):
          """重写父类方法, 用例非期望成功时在stopTest前调用"""
          test.result.status = TestStatus.XPASS
          self.expectedFailures_count += 1
          super().addUnexpectedSuccess(test)

  测试本TestResult类方法

 if __name__ == '__main__':
      import unittest
      class TestDemo(unittest.TestCase):  
          def test_a(self):  # 可以添加更多的用例进行测试
              """测试a
              tag:smoke
              tag:demo
              level: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 os
  def 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 = err
          while tb and self._is_relevant_tb_level(tb):
              tb = tb.tb_next
          if exctype is test.failureException:
              # Skip assert*() traceback levels
              length = self._count_relevant_tb_levels(tb)
          else:
              length = None
          tb_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 = err
          exc_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)icon-default.png?t=N5F7http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=hIqEQD5B5ZyLT0S-vFq64p5MCDBc8jJU&authKey=O%2B3T95fjNUNsYxXnPIrOxvkb%2BbuFd1AxuUP5gCbos34AQDjaRG2L6%2Fm9gGakvo94&noverify=0&group_code=574737577

Unittest二次开发实战_第1张图片

 

 

 

你可能感兴趣的:(软件测试工具,web自动化测试,软件测试,python,开发语言,测试用例,web自动化测试,数据库)