zoukankan      html  css  js  c++  java
  • 基于Django Admin+HttpRunner-1.5.6开发简易的接口测试平台

    前言

    这是一个使用HttpRunner开发接口平台的简单Demo。

    新建Django项目

    安装依赖包

    pip install httprunner=1.5.6 -i https://pypi.doubanio.com/simple/
    

    模型规划

    • 项目Project:包含 名称、创建时间、修改时间
    • 测试套件TestSuite:对应HttpRunner的一个yaml文件,包含所属项目、name、base_url、request请求配置、variables用户自定义变量、创建时间、修改时间
    • 测试用例TestCase:对应HttpRunner中的一个test段,包含所属TestSuite、name、skip、request、validate、extract、创建时间、修改时间
    • 测试结果TestResult:测试套件运行的一次结果信息,包含所属TestSuite、HttpRunner运行summary中的时间信息、统计信息、平台信息、详情等

    自定义YamlField

    由于TestSuite中的request、variables以及用例中的request我们需要使用Python的字典格式,用例中的validate和extract需要使用Python的列表格式。而Django中这些只能按字符串格式TextField存储。
    我们编写一个自定义YamlField,存库时按字符串存,读取时转为Python字典或列表。
    在apitest目录下新建fields.py,内容如下。

    import yaml
    from django.db import models
    
    class YamlField(models.TextField):
        def to_python(self, value):  # 将数据库内容转为python对象时调用
            if not value:
                value = {}
            if isinstance(value, (list, dict)):
                return value
            return yaml.safe_load(value)
    
        def get_prep_value(self, value):  # create时插入数据, 转为字符串存储
            return value if value is None else yaml.dump(value, default_flow_style=False)
    
        def from_db_value(self, value, expression, connection):  # 从数据库读取字段是调用
            return self.to_python(value)
    

    使用抽象模型

    由于好几个项目、测试套件、测试用例都需要名称、创建时间、修改时间三个属性。为了简化代码,这里创建一个抽象模型ModelWithName,抽象模型用来通过继承来复用属性,并不会创建表。
    修改apitest/models.py,添加:

    from django.db import models
    class ModelWithName(models.Model):
        class Meta:
            abstract = True
    
        name = models.CharField("名称", max_length=200)
        created = models.DateTimeField('创建时间', auto_now_add=True)
        modified = models.DateTimeField('最后修改时间', auto_now=True)
        def __str__(self):
            return self.name
    

    编写模型

    修改apitest/models.py,添加:

    class Project(ModelWithName):
        class Meta:
            verbose_name_plural = verbose_name = '项目'
    
    
    class TestSuite(ModelWithName):
        """对应httprunner的一个yaml文件"""
        class Meta:
            verbose_name_plural = verbose_name = '测试套件'
        project = models.ForeignKey(Project, verbose_name='项目', related_name='suites', on_delete=models.CASCADE)
        base_url = models.CharField('域名', max_length=500, blank=True, null=True)  # 对应config/base_url
        request = YamlField('请求默认配置', blank=True)  # 对应config/request
        variables = YamlField('变量', blank=True)
    
    class TestCase(ModelWithName):
        """对应httprunner中的一个test"""
        class Meta:
            verbose_name_plural = verbose_name = '测试用例'
    
        suite = models.ForeignKey(TestSuite, verbose_name='测试套件', related_name='tests', on_delete=models.CASCADE)
        skip = models.BooleanField('跳过', default=False)
        request = YamlField('请求数据')  # 对应config/request
        extract = YamlField('提取请求', blank=True)
        validate = YamlField('断言', blank=True)
    
    class TestResult(models.Model):
        class Meta:
            verbose_name_plural = verbose_name = '测试结果'
    
        suite = models.ForeignKey(TestSuite, verbose_name='测试套件', related_name='results', on_delete=models.CASCADE)
        success = models.BooleanField('成功')
        start_at = models.DateTimeField('开始时间')
        duration = models.DurationField('持续时间')
        platform = models.TextField('平台信息')
        test_run = models.SmallIntegerField('运行')
        successes = models.SmallIntegerField('成功')
        skipped = models.SmallIntegerField('跳过')
        failures = models.SmallIntegerField('失败')
        errors = models.SmallIntegerField('出错')
        expected_failures = models.SmallIntegerField('预期失败')
        unexpected_successes = models.SmallIntegerField('非预期成功')
        details = models.TextField('详情')
        created = models.DateTimeField('创建时间', auto_now_add=True)
    
        def __str__(self):
            return self.suite.name + '-测试结果'
    

    HttpRunner运行结果的summary的格式如下:

     {'platform': {'httprunner_version': '1.5.6', 'platform': 'Darwin-19.2.0-x86_64-i386-64bit', 'python_version': 'CPython 3.6.5'},
     'stat': {'errors': 0, 'expectedFailures': 0,'failures': 0,'skipped': 0,'successes': 1,'testsRun': 1,'unexpectedSuccesses': 0},
     'success': True,
     'time': {'duration': 2.2655465602874756, 'start_at': 1587895780.3771362}}
     'details': [   # 每个对应一个测试套件
        {'name': '套件名称',
         'base_url': 'https://httpbin.org',
         'stat':  {'errors': 0, 'expectedFailures': 0,'failures': 0,'skipped': 0,'successes': 1,'testsRun': 1,'unexpectedSuccesses': 0},
         'success': True,
         'time': {'duration': 2.2655465602874756, 'start_at': 1587895780.3771362}},
         'output': [],
         'records': [   # 对应每一条用例
             {
               'name': '用例名',
               'status': 'success',
               'meta_data': {'request': {'url': ..., 'method': ..., 'start_timestamp': ...}, 
                                      'response': {'content': ..., 'text': ..., 'json': ..., 'headers': ..., 'status_code': ..., 'elapsed_ms': ...}}
               'attachment': ['出错信息']
             }
         ]
     }
    

    这里TestResult模型,对summary结果的信息做了简单的拆解。

    组装用例数据

    对于用例TestCase,我们需要将其name、skip、request、validate、extract组装成HttpRunner的字典格式。
    在apitest/models.py的TestCase类中添加data属性方法,代码如下:

    class TestCase(ModelWithName):
        ....
        @property
        def data(self):
            return dict(name=self.name,skip=self.skip,request=self.request,extract=self.extract,validate=self.validate)
    

    组装测试套件数据

    一个套件最后解析后应该是包含name、config、apis、testcases的一个字典,我们需要将TestSuite对象及包含的所有TestCase对象组装成如下格式。

    {"name": "套件名称", "config" : {...}, "apis": {}, "testcases": []}
    

    在apitest/models.py的TestSuite类中添加data属性方法,代码如下:

    @property
        def data(self):
            request = self.request
            request['base_url'] = self.base_url
            data = dict(
                name=self.name,
                config=dict(request=self.request, variables=self.variables),
                api={},
                testcases=[test.data for test in self.tests.all()]
            )
            return data
    

    由于TestCase在外联TestSuite时设置了关联名称tests,因此TestSuite对象可以通过self.tests.all()查询出所有关联它的用例。

    注:HttpRunner-1.5.6版本的base_url是放在config/request中的,这里做了分离,要重新放入config/request中。

    编写套件运行方法

    从 httprunner.task模块中导入HttpRunner类,使用TestSuite数据,运行即可。由于运行时是安多个TestSuite模式运行的,因此TestSuite的数据要放到一个列表中。
    在apitest/models.py的TestSuite类添加run方法。

    from httprunner.task import HttpRunner
    ...
    
    class TestSuite(ModelWithName):
        ...
        def run(self):
            runner = HttpRunner().run([self.data])
            summary = runner.summary
            if summary:
                # 保存结果到TestResult
                _time = summary['time']
                _stat = summary['stat']
                TestResult.objects.create(
                    suite=self, success=summary['success'],
                    start_at=datetime.datetime.fromtimestamp(_time['start_at']),
                    duration=datetime.timedelta(seconds=_time['duration']),
                    test_run=_stat['testsRun'], successes=_stat['successes'], skipped=_stat['skipped'], errors=_stat['errors'],
                    failures=_stat['failures'], expected_failures=_stat['expectedFailures'],
                    unexpected_successes=_stat['unexpectedSuccesses'],
                    platform=json.dumps(summary['platform'], indent=2, ensure_ascii=False),
                    details=summary['details']
                )
            return summary
    

    运行后,解析summary并创建TestResult对象保存本次运行结果。

    模型完整代码

    import datetime
    import json
    
    from django.db import models
    from httprunner.task import HttpRunner
    
    from .fields import YamlField
    
    
    class ModelWithName(models.Model):
        class Meta:
            abstract = True
    
        name = models.CharField("名称", max_length=200)
        created = models.DateTimeField('创建时间', auto_now_add=True)
        modified = models.DateTimeField('最后修改时间', auto_now=True)
        
        def __str__(self):
            return self.name
    
    class Project(ModelWithName):
        class Meta:
            verbose_name_plural = verbose_name = '项目'
    
    
    class TestSuite(ModelWithName):
        """对应httprunner的一个yaml文件"""
        class Meta:
            verbose_name_plural = verbose_name = '测试套件'
        project = models.ForeignKey(Project, verbose_name='项目', related_name='suites', on_delete=models.CASCADE)
        base_url = models.CharField('域名', max_length=500, blank=True, null=True)  # 对应config/base_url
        request = YamlField('请求默认配置', blank=True)  # 对应config/request
        variables = YamlField('变量', blank=True)
    
        @property
        def data(self):
            request = self.request
            request['base_url'] = self.base_url
            data = dict(
                name=self.name,
                config=dict(request=self.request, variables=self.variables),
                api={},
                testcases=[test.data for test in self.tests.all()]
            )
            return data
    
        def run(self):
            runner = HttpRunner().run([self.data])
            summary = runner.summary
            if summary:
                # 保存结果到TestResult
                _time = summary['time']
                _stat = summary['stat']
                TestResult.objects.create(
                    suite=self, success=summary['success'],
                    start_at=datetime.datetime.fromtimestamp(_time['start_at']),
                    duration=datetime.timedelta(seconds=_time['duration']),
                    test_run=_stat['testsRun'], successes=_stat['successes'], skipped=_stat['skipped'], errors=_stat['errors'],
                    failures=_stat['failures'], expected_failures=_stat['expectedFailures'],
                    unexpected_successes=_stat['unexpectedSuccesses'],
                    platform=json.dumps(summary['platform'], indent=2, ensure_ascii=False),
                    details=summary['details']
                )
            return summary
    
    
    class TestCase(ModelWithName):
        """对应httprunner中的一个test"""
        class Meta:
            verbose_name_plural = verbose_name = '测试用例'
    
        suite = models.ForeignKey(TestSuite, verbose_name='测试套件', related_name='tests', on_delete=models.CASCADE)
        skip = models.BooleanField('跳过', default=False)
        request = YamlField('请求数据')  # 对应config/request
        extract = YamlField('提取请求', blank=True)
        validate = YamlField('断言', blank=True)
    
        @property
        def data(self):
            return dict(name=self.name,skip=self.skip,request=self.request,extract=self.extract,validate=self.validate)
    
    
    class TestResult(models.Model):
        class Meta:
            verbose_name_plural = verbose_name = '测试结果'
    
        suite = models.ForeignKey(TestSuite, verbose_name='测试套件', related_name='results', on_delete=models.CASCADE)
        success = models.BooleanField('成功')
        start_at = models.DateTimeField('开始时间')
        duration = models.DurationField('持续时间')
        platform = models.TextField('平台信息')
        test_run = models.SmallIntegerField('运行')
        successes = models.SmallIntegerField('成功')
        skipped = models.SmallIntegerField('跳过')
        failures = models.SmallIntegerField('失败')
        errors = models.SmallIntegerField('出错')
        expected_failures = models.SmallIntegerField('预期失败')
        unexpected_successes = models.SmallIntegerField('非预期成功')
        details = models.TextField('详情')
        created = models.DateTimeField('创建时间', auto_now_add=True)
    
        def __str__(self):
            return self.suite.name + '-测试结果'
    

    使用Django Admin

    修改apitest/admin.py,代码如下:

    from django.contrib import admin
    
    from apitest import models
    
    
    @admin.register(models.Project)
    class ProjectAdmin(admin.ModelAdmin):
        list_display = ('name', 'created', 'modified')
    
    
    class TestCaseInline(admin.StackedInline):
        model = models.TestCase
        extra = 1
    
    
    @admin.register(models.TestSuite)
    class TestSuiteAdmin(admin.ModelAdmin):
        inlines = [TestCaseInline]
        list_display = ('name', 'project', 'base_url', 'created', 'modified')
        list_filter = ('project', )
    
        actions = ("run", )
    
        def run(self, request, queryset):
            for suite in queryset:
                suite.run()
        run.short_description = "运行"
    
    
    @admin.register(models.TestResult)
    class TestResultAdmin(admin.ModelAdmin):
        readonly_fields = ('suite', 'success', 'start_at', 'duration', 'platform',
                           'test_run', 'successes', 'skipped', 'failures', 'errors',
                           'expected_failures', 'unexpected_successes', 'details', 'created')
        fields = (('suite', 'success'),
                  ('start_at', 'duration'),
                  ('platform',),
                  ('test_run', 'successes', 'skipped', 'failures', 'errors', 'expected_failures', 'unexpected_successes'),
                  ('details',)
                  )
        list_display = ('suite', 'success', 'test_run', 'successes', 'errors', 'failures', 'start_at', 'duration')
        list_filter = ('suite', )
    

    这里将项目、测试套件、测试结果三个模型注册到Admin后台,测试用例则作为内联模型放到测试套件中进行编辑。
    在测试套件模型中,自定义了一个“运行”,操作,支持运行选中的用例。

    运行并测试项目

    打开terminal终端,执行数据库变更并创建超级管理员。

    python3 manage.py makemigrations
    python3 manage.py migrate
    python3 manage.py createsuperuser
    

    运行开发服务器

    python3 manage.py runserver
    

    访问http://127.0.0.1:8000/admin并登录。

    创建一个项目,测试项目,然后创建一个TestSuite,如下:

    请求默认配置:

    headers:
      x-text: abc123
    

    变量:

    a: 1
    b: 2
    

    请求数据:

    url: /get
    method: GET
    params: 
      a: $a
      b: $b
    

    提取请求:

    - res_url: content.url
    

    断言:

    - eq: [status_code, 200]
    

    点击保存。

    回到TestSuite列表,选中测试套件,动作下拉框中选择“运行”,点击Go按钮。

    返回测试结果列表、查看测试结果。

    程序代码https://github.com/hanzhichao/apirunner

  • 相关阅读:
    函数声明、引用
    事件绑定的快捷方式 利on进行事件绑定的几种情况
    BOM的节点方法和属性
    JQuery语法 JQuery对象与原生对象互转 文档就绪函数与window.onload的区别
    JPEG解码:huffman解码
    Quartus 中快速分配器件管脚
    PLL的modelsim仿真
    JPEG解码:桶型寄存器
    JPEG解码:反DCT变换(二)
    JPEG解码:反DCT变换(一)
  • 原文地址:https://www.cnblogs.com/superhin/p/12781774.html
Copyright © 2011-2022 走看看