通过Django Admin+HttpRunner1.5.6实现简易接口测试平台


Posted in Python onNovember 11, 2020

前言

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

新建Django项目

通过Django Admin+HttpRunner1.5.6实现简易接口测试平台

安装依赖包

通过Django Admin+HttpRunner1.5.6实现简易接口测试平台

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,内容如下。

串存,读取时转为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": []}

补充:加载debugtalk.py的方法
config中可以指定一个yaml的path路径,会自动加载该路径下的debugtalk.py文件

- utils
   - config.yaml # 空文件即可
   - debugtalk.py

config的格式可以为:

config: 
   name: ...
   request: ...
   variables: ...
   path: .../config.yaml

这样可以自动加载debugtalk.py中的函数以供使用。

在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并登录。

通过Django Admin+HttpRunner1.5.6实现简易接口测试平台

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

通过Django Admin+HttpRunner1.5.6实现简易接口测试平台

请求默认配置:

headers: x-text: abc123

变量:

a: 1b: 2

通过Django Admin+HttpRunner1.5.6实现简易接口测试平台

请求数据:

url: /getmethod: GETparams: a: $a b: $b

提取请求:

- res_url: content.url

断言:

- eq: [status_code, 200]

点击保存。

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

通过Django Admin+HttpRunner1.5.6实现简易接口测试平台

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

通过Django Admin+HttpRunner1.5.6实现简易接口测试平台

通过Django Admin+HttpRunner1.5.6实现简易接口测试平台

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

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持三水点靠木。

Python 相关文章推荐
Python实现多并发访问网站功能示例
Jun 19 Python
在Mac上删除自己安装的Python方法
Oct 29 Python
使用Python向DataFrame中指定位置添加一列或多列的方法
Jan 29 Python
10分钟教你用python动画演示深度优先算法搜寻逃出迷宫的路径
Aug 12 Python
详解Python并发编程之创建多线程的几种方法
Aug 23 Python
详解Python实现进度条的4种方式
Jan 15 Python
Python实现新型冠状病毒传播模型及预测代码实例
Feb 05 Python
浅谈Python中的异常和JSON读写数据的实现
Feb 27 Python
python实现opencv+scoket网络实时图传
Mar 20 Python
如何提高python 中for循环的效率
Apr 15 Python
selenium切换标签页解决get超时问题的完整代码
Aug 30 Python
python 解决Windows平台上路径有空格的问题
Nov 10 Python
Django自定义YamlField实现过程解析
Nov 11 #Python
Python监听剪切板实现方法代码实例
Nov 11 #Python
如何通过python计算圆周率PI
Nov 11 #Python
python中turtle库的简单使用教程
Nov 11 #Python
python 怎样进行内存管理
Nov 10 #Python
python tqdm实现进度条的示例代码
Nov 10 #Python
python 解决Windows平台上路径有空格的问题
Nov 10 #Python
You might like
解析thinkphp基本配置 convention.php
2013/06/18 PHP
两级联动select刷新后其值保持不变的实现方法
2014/01/27 PHP
ThinkPHP自动完成中使用函数与回调方法实例
2014/11/29 PHP
PHP的swoole扩展安装方法详细教程
2016/05/18 PHP
php 解析xml 的四种方法详细介绍
2016/10/26 PHP
使用laravel和ajax实现整个页面无刷新的操作方法
2019/10/03 PHP
Laravel实现搜索的时候分页并携带参数
2019/10/15 PHP
jQuery 页面载入进度条实现代码
2009/02/08 Javascript
jQuery实现表单input中提示文字value随鼠标焦点移进移出而显示或隐藏的代码
2010/03/21 Javascript
JQuery UI DatePicker中z-index默认为1的解决办法
2010/09/28 Javascript
动态创建script在IE中缓存js文件时导致编码的解决方法
2014/05/04 Javascript
jQuery简单实现两级下拉菜单效果代码
2015/09/15 Javascript
详解nodejs微信公众号开发——1.接入微信公众号
2017/04/10 NodeJs
浅析webpack 如何优雅的使用tree-shaking(摇树优化)
2017/08/16 Javascript
JQuery实现table中tr上移下移的示例(超简单)
2018/01/08 jQuery
在Vue中使用highCharts绘制3d饼图的方法
2018/02/08 Javascript
微信小程序定位当前城市的方法
2018/07/19 Javascript
JavaScript递归函数定义与用法实例分析
2019/01/24 Javascript
微信小程序调用微信支付接口的实现方法
2019/04/29 Javascript
Vue优化:常见会导致内存泄漏问题及优化详解
2020/08/04 Javascript
[03:09]2014DOTA2国际邀请赛 Mushi前队友送上祝福
2014/07/12 DOTA
python使用新浪微博api上传图片到微博示例
2014/01/10 Python
Python continue语句用法实例
2014/03/11 Python
Python使用smtplib模块发送电子邮件的流程详解
2016/06/27 Python
Python字符串处理实现单词反转
2017/06/14 Python
Python利用字典将两个通讯录文本合并为一个文本实例
2018/01/16 Python
python numpy和list查询其中某个数的个数及定位方法
2018/06/27 Python
Django中日期处理注意事项与自定义时间格式转换详解
2018/08/06 Python
Python神奇的内置函数locals的实例讲解
2019/02/22 Python
解决python3.x安装numpy成功但import出错的问题
2020/11/17 Python
移动端html5模拟长按事件的实现方法
2018/09/30 HTML / CSS
五年级语文教学反思
2014/01/30 职场文书
工程安全员岗位职责
2014/03/09 职场文书
2014年环境整治工作总结
2014/12/10 职场文书
毕业设计论文评语
2014/12/31 职场文书
煤矿安全生产工作总结
2015/08/13 职场文书