用例执行的异步处理
使用之前的代码修改、结合task执行异步任务,需要特别注意的是这里是异步调用,task和run_cases_q不是在一个进程中。
按照我们之前的理解来改写;admin.class CaseAdmin
![](https://img-blog.csdnimg.cn/img_convert/253429efe0e5dd789b0fa5e2b356c838.png)
tasks.py
![](https://img-blog.csdnimg.cn/img_convert/cb9dfb759a9eba2409faf426950b5749.png)
页面上运行一下发现报错了,queryset不能被序列化为json
![](https://img-blog.csdnimg.cn/img_convert/dad12dfe321ec446358bc26cb365357d.png)
需要注意的是tasks.run_cases.delay(cases, request.user)和run_cases(cases, user) 并不是直接通过参数传递的。它需要把delay的对象序列化成json,就算能转换也非常大,浪费队列资源。
我们转变下思路,用django的对象关系映射 (ORM,object-relational mapping)转换下来解决
admin.class CaseAdmin
def run_cases_q(self, request, queryset):
logger = logging.getLogger('test_plt')
suite_ctx = {}
cases = queryset.order_by('reorder').all()
case_ids = [case.id for case in cases]
result = tasks.run_cases.delay(case_ids, request.user.id)
self.message_user(request, f'用例任务执行已列队,任务ID{result}')
return HttpResponseRedirect(f'/admin/test_plt/caserunlog')
run_cases_q.short_description = '队列执行 用例'
tasks.py
@shared_task()
def run_cases(cases_ids, user_id):
logger = logging.getLogger('test_plt')
suite_ctx = {}
flag = False # 事先声明,假如没有获取到cases_ids。那么直接算失败
for cases_id in cases_ids:
case = Case.objects.get(id=cases_id)
user = User.objects.get(id=user_id)
# 执行单个用例
flag = common.perform_case(case, user, suite_ctx=suite_ctx)
if not flag and case.abort_when_fail:
logger.info(f'【{case.name}】 校验失败,原因:有用例,接口执行失败,且要求用例执行终止')
break
return flag
接下来是用例执行返回消息的问题需要解决
![](https://img-blog.csdnimg.cn/img_convert/467722bae7fff1b67be0a71e59ca310b.png)
之前手动调用时我们发现delay生产出的任务是 AsyncResult
![](https://img-blog.csdnimg.cn/img_convert/a8cd255b2acd4666513fb93e205a2e0c.png)
在pycharm里双击shift键搜索下,点击过去查看下他的返回
![](https://img-blog.csdnimg.cn/img_convert/99854a80241cc759a4da92c6e1c86f2a.png)
![](https://img-blog.csdnimg.cn/img_convert/f08e07620557d7b1dd9eb29a6f1edae1.png)
看下 str,就是返回的task的uuid。这里需要明确的是delay用了是马上返回taskid,它的返回值和tasks.run_cases 方法没有任何关系。所以返回的根本就不是flag。 workrer执行完了以后才会返回tasks.run_cases方法里的值。
完成后执行下发下能够跑通,暂时没有问题
用例套件的异步执行
接下来在用例套件的代码上复用和修改
# tasks.py
@shared_task()
def run_suites(suite_ids, user_id):
logger = logging.getLogger('test_plt')
proj_ctx = {} # 多个套件一起执行,我们虚拟一个项目上下文的参数,用于跨套件的参数传递.
flag = False
for suite_id in suite_ids:
suite = CaseSuite.objects.get(id=suite_id)
user = User.objects.get(id=user_id)
logger.info(f'【{suite.name}】 套件开始执行')
suite_log = common.push_suite_runlog(suite, user=user)
suite_ctx = {}
for case in suite.cases.order_by('reorder'): # type: Case
flag = common.perform_case(case, user, case_suite=suite, case_suitelog=suite_log,
suite_ctx=suite_ctx, proj_ctx=proj_ctx)
if not flag and case.abort_when_fail:
errmsg = f'【{case.name}】 执行失败,原因:有用例接口执行失败,且要求用例执行终止'
logger.info(errmsg)
common.push_suite_runlog(suite, suite_log=suite_log, passed=False, err_msg=errmsg)
break
common.push_suite_runlog(suite, suite_log=suite_log, passed=True)
logger.info(f'【{suite.name}】 套件执行完毕')
return flag
# admin.py/CaseSuiteAdmin
def run_suites_q(self, request, queryset):
suites = queryset.all()
suite_ids = [suite.id for suite in suites]
result = tasks.run_suites.delay(suite_ids, request.user.id)
self.message_user(request, f'用例套件任务执行已列队,任务ID{result}')
return HttpResponseRedirect(f'/admin/test_plt/casesuiterunlog')
![](https://img-blog.csdnimg.cn/img_convert/578854d3c3cc176af1bee26bd9dcd9d9.png)
运行后没有大问题,但是运行过程中有关于时间的警告,我们使用 start_at = 和 finish_at = 在文件中查找看下发现是之前所有关于log.XX_at的时间方法都漏写了timezone.make_aware( ),补上即可.
![](https://img-blog.csdnimg.cn/img_convert/c896022f4ee1b835206618755ebead9b.png)
周期任务的实战使用
接下来我们在jango页面上创建一个周期任务,查看是否能正常触发用例套件的执行:
![](https://img-blog.csdnimg.cn/img_convert/c561de3bdb1149eeda91724d65f57e26.png)
周期任务由celery_beat驱动
需要注意的是Arguments 的位置参数对应的是 tasks.py,比如上方任务里选择的是 run_cases,那么位置参数对应的就是 run_cases(cases_ids, user_id),如果我想要执行用例里的 用户登录、订单统计,那么只需要填写它们的id
![](https://img-blog.csdnimg.cn/img_convert/bbd2906c1a041728f11f704f7fda74e4.png)
[[1,2],1]; [1,2]对应cases_ids,1对应user_id;逗号分隔。
运行用例,记录测试批次
手动执行或通过定时任务执行用例/套件现在没有区分开,在页面上无法展示出这些用例/套件是哪一个批次执行的。现在需要优化的就是这一点
# models.py创建类,落库为表
class TestBatchCase(models.Model):
id = models.AutoField(primary_key=True)
test_batch = models.ForeignKey(TestBatch, on_delete=models.CASCADE, related_name='cases', verbose_name='测试批次')
case = models.ForeignKey(Case, on_delete=models.CASCADE, verbose_name='测试用例')
def __str__(self):
return f'{self.test_batch} at {self.case}'
class Meta:
verbose_name = '测试批次-用例'
verbose_name_plural = verbose_name
db_table = 'test_plt_testbatch_cases'
class TestBatchCaseSuite(models.Model):
id = models.AutoField(primary_key=True)
test_batch = models.ForeignKey(TestBatch, on_delete=models.CASCADE, related_name='suite', verbose_name='测试批次')
case_suite = models.ForeignKey(CaseSuite, on_delete=models.CASCADE, verbose_name='测试套件')
def __str__(self):
return f'{self.test_batch} at {self.case_suite}'
class Meta:
verbose_name = '测试批次-套件'
verbose_name_plural = verbose_name
db_table = 'test_plt_testbatch_casesuite'
另外 CaseSuiteRunLog 和 CaseRunLog 里也需要加上字段
# CaseRunLog
test_batch = models.ForeignKey(TestBatch, blank=True, null=True, on_delete=models.CASCADE,related_name='case_runlogs', verbose_name='测试批次')
# CaseSuiteRunLog
test_batch = models.ForeignKey(TestBatch, blank=True, null=True, on_delete=models.CASCADE,related_name='suite_runlogs', verbose_name='测试批次')
接下来就是运行用例的时候TestBatchCase需要记录的东西
# 进行改造
def run_cases_q(self, request, queryset):
cases = queryset.order_by('reorder').all()
case_ids = [case.id for case in cases]
bat = TestBatch.objects.create(
pj=cases[0].pj,
created_by=request.user,
start_at=timezone.now(),
run_type=TestBatch.RUN_TYPE_QUEUE,
obj_type=TestBatch.OBJ_TYPE_CASE,
status=TestBatch.STATUS_PENDING
)
# 下面需要处理从表TestBatchCase关联表的信息,因为之前我们反向定义了表名为‘cases’
for case in cases:
bat.cases.create(case=case, test_batch=bat)
tasks.run_cases.delay(case_ids, request.user.id, bat.id)
self.message_user(request, f'用例任务执行已列队,任务批次ID{bat.id}')
return HttpResponseRedirect(f'/admin/test_plt/testbatch/{bat.id}')
用例执行完成后更新TestBatchCase表,需要注意的是perform_case、push_case_runlog、push_suite_runlog 方法中都需要接收test_batch=None。
# tasks.py
@shared_task()
def run_cases(cases_ids, user_id, bat_id):
logger = logging.getLogger('test_plt')
# 异步调用基本没办法断点调试,所以打印参数方便后续查看
logger.info(f'run_cases task start: case_ids={cases_ids}; bat_id={bat_id}; user_id={user_id}')
suite_ctx = {} # 讲道理多用例的执行并不能算套件,但是考虑到用户可能会在用例执行的页面,一次执行多条用例且需要用例参数传递。故此获取所选的用例,按执行顺序排列
flag = False # 事先声明,假如没有获取到cases_ids。那么直接算失败
bat = TestBatch.objects.get(id=bat_id)
try:
for cases_id in cases_ids:
case = Case.objects.get(id=cases_id)
user = User.objects.get(id=user_id)
# 执行单个用例
flag = common.perform_case(case, user, suite_ctx=suite_ctx, test_batch=bat)
if not flag and case.abort_when_fail:
logger.info(f'【{case.name}】 校验失败,原因:有用例,接口执行失败,且要求用例执行终止')
break
bat.status = TestBatch.STATUS_FINISHED
except Exception as e:
bat.status = TestBatch.STATUS_FAILED
bat.error_msg = str(e)
bat.finish_at = timezone.now()
bat.save()
logger.info(f'run_cases task finish')
return flag
TestBatch admin页面的新样式
@admin.register(TestBatch)
class TestBatchAdmin(ModelAdmin):
def has_delete_permission(self, request, obj=None):
return False
def has_change_permission(self, request, obj=None):
return False
def has_add_permission(self, request):
return False
list_display = ['id', 'pj', 'start_at', 'finish_at', 'obj_type', 'run_type', 'periodic_task', 'status']
list_display_links = ['id', 'start_at']
list_filter = ['run_type', 'obj_type', 'status']
search_fields = []
inlines = [CaseSuiteRunLogNestedInline, CaseRunlogNestedInline]
fieldsets = (
('基础信息', {
'fields': (('id', 'pj'), ('status', 'created_by'), ('obj_type', 'run_type', 'periodic_task'),
'cost_time', 'error_msg')
}),
('统计信息', {
'fields': (('stat_suite_plan', 'stat_suite_run', 'stat_suite_success', 'stat_suite_success_rto'),
('stat_case_plan', 'stat_case_run', 'stat_case_success', 'stat_case_success_rto'),
('stat_api_plan', 'stat_api_run', 'stat_api_success', 'stat_api_success_rto'),)
})
)
cost_time 对象
def cost_time(self, obj: CaseRunLog):
if obj.finish_at:
delta: timedelta = obj.finish_at - obj.start_at
return common.fmt_cost_time(obj.start_at, obj.finish_at, delta.seconds * 1000 + delta.microseconds / 1000)
else:
return common.fmt_local_datetime(obj.start_at)
还需要根据obj_type的类型来展示相应的内联表,使用get_inline_instances 回调函数
def get_inline_instances(self, request, obj=None):
if obj.obj_type == TestBatch.OBJ_TYPE_CASE:
self.inlines = [CaseRunlogNestedInline]
else:
self.inlines = [CaseSuiteRunLogNestedInline]
return super().get_inline_instances(request, obj)
需要注意的是,这里是方法重写,原本方法中有return。
运行套件,记录测试批次
参考用例运行逻辑稍作修改
@shared_task()
def run_suites(suite_ids, user_id, bat_id):
logger = logging.getLogger('test_plt')
logger.info(f'run_suites task start: case_ids={suite_ids}; bat_id={bat_id}; user_id={user_id}')
proj_ctx = {} # 多个套件一起执行,我们虚拟一个项目上下文的参数,用于跨套件的参数传递.
flag = False
bat = TestBatch.objects.get(id=bat_id)
try:
for suite_id in suite_ids:
suite = CaseSuite.objects.get(id=suite_id)
user = User.objects.get(id=user_id)
logger.info(f'【{suite.name}】 套件开始执行')
suite_log = common.push_suite_runlog(suite, user=user, test_batch=bat)
suite_ctx = {}
for case in suite.cases.order_by('reorder'): # type: Case
flag = common.perform_case(case, user, case_suite=suite, case_suitelog=suite_log,
suite_ctx=suite_ctx, proj_ctx=proj_ctx)
if not flag and case.abort_when_fail:
errmsg = f'【{case.name}】 执行失败,原因:有用例接口执行失败,且要求用例执行终止'
logger.info(errmsg)
common.push_suite_runlog(suite, suite_log=suite_log, passed=False, err_msg=errmsg)
break
common.push_suite_runlog(suite, suite_log=suite_log, passed=True)
logger.info(f'【{suite.name}】 套件执行完毕')
bat.status = TestBatch.STATUS_FINISHED
except Exception as e:
bat.status = TestBatch.STATUS_FAILED
bat.error_msg = str(e)
bat.finish_at = timezone.now()
bat.save()
logger.info(f'run_suites task finish')
return flag
周期性任务的修改
我们用周期性任务执行task的时候设置的传参,目前是用户可以在页面选中用例/套件异步执行,但是通过周期任务执行我们不知道 bat_id,所以在代码和逻辑上就需要优化
![](https://img-blog.csdnimg.cn/img_convert/14fb3d3a31c42a6c2f55e90f4077a271.png)
![](https://img-blog.csdnimg.cn/img_convert/34cfedc13838a11a92d6f20ccbae9389.png)
单独为周期任务创建新的task,因为免去了用户在页面执行,所以不会执行admin.py 里的 TestBatch.objects.create(),我们把这一段代码放到task中
def run_cases(cases_ids, user_id, bat_id):
logger = logging.getLogger('test_plt')
# 异步调用基本没办法断点调试,所以打印参数方便后续查看
logger.info(f'run_cases task start: case_ids={cases_ids}; bat_id={bat_id}; user_id={user_id}')
suite_ctx = {} # 讲道理多用例的执行并不能算套件,但是考虑到用户可能会在用例执行的页面,一次执行多条用例且需要用例参数传递。故此获取所选的用例,按执行顺序排列
flag = False # 事先声明,假如没有获取到cases_ids。那么直接算失败
bat = TestBatch.objects.get(id=bat_id)
try:
for cases_id in cases_ids:
case = Case.objects.get(id=cases_id)
user = User.objects.get(id=user_id)
# 执行单个用例
flag = common.perform_case(case, user, suite_ctx=suite_ctx, test_batch=bat)
if not flag and case.abort_when_fail:
logger.info(f'【{case.name}】 校验失败,原因:有用例,接口执行失败,且要求用例执行终止')
break
bat.status = TestBatch.STATUS_FINISHED
except Exception as e:
bat.status = TestBatch.STATUS_FAILED
bat.error_msg = str(e)
bat.finish_at = timezone.now()
bat.save()
logger.info(f'run_cases task finish')
return flag
@shared_task()
def run_cases_periodic(case_ids, user_id):
# 使用for循环取cases效率太低,这里我们直接用用户写的cases_ids的第一条来判断属于哪个项目
# cases = [Case.objects.get(id=caid) for caid in case_ids]
case = Case.objects.get(id=case_ids[0])
bat = TestBatch.objects.create(
pj=case.pj,
created_by_id=user_id,
start_at=timezone.now(),
run_type=TestBatch.RUN_TYPE_PERIODIC,
obj_type=TestBatch.OBJ_TYPE_CASE,
status=TestBatch.STATUS_PENDING
)
# 下面需要处理从表TestBatchCase关联表的信息,因为之前我们反向定义了表名为‘cases’
for cid in case_ids:
bat.cases.create(case_id=cid, test_batch=bat)
return run_cases(case_ids, user_id, bat.id)
套件修改同上,完成后运行发下没有记录计划任务的名称
![](https://img-blog.csdnimg.cn/img_convert/15ef366f888cec96343326da02e1d2d7.png)
此时我们需要给run_cases_periodic方法多传一个 periodic_task_id=None , 并且create 时增加periodic_task_id。但又因为 task是由bat调用,按照规则我们需要继承方法并重写,涉及到CELERY_BEAT_SCHEDULER
# 在sttings同级目录下创建 schedulers.py
from django_celery_beat import schedulers
class TestPltModelEntry(schedulers.ModelEntry):
"""
继承并自定义ModelEntry,重写部分构造函数
"""
def __init__(self, model, app=None):
"""
向task传递periodic_task_id(作为keyword命名参数)
:param model:
:param app:
"""
super().__init__(model, app)
self.kwargs["periodic_task_id"] = model.pk
class TestPltDatabaseScheduler(schedulers.DatabaseScheduler):
"""
覆盖 DatabaseScheduler 的 Entry
"""
Entry = TestPltModelEntry
![](https://img-blog.csdnimg.cn/img_convert/dfedb47f13cb3bfbf07459e12eb7b541.png)
run_suites_periodic run_case_periodic 加上参数。再启用周期性任务中任务即可
![](https://img-blog.csdnimg.cn/img_convert/807eed00ab9fa047994f4e1dd252b6f5.png)
各类统计参数
用例/套件 通过率、成功、失败等数据的记录。这些数据的统计应该在用例/套件执行完成之后统计
举个例子:
在套件页面执行,在这一个批次执行完成后test_batch_id(批次id)=1,我想要统计测试套件执行了几个。 TestBatch 这个数据库表中查 self.obj_type == 按套件执行,在通过反向 self.suites 就可以找到testbatch_casesuite 这个表里 test_batch_id=1 的所有对象。然后在 .cont()统计即可
# model.py/class TestBatch
def stat(self):
# 套件的 4个指标
if self.obj_type == TestBatch.OBJ_TYPE_SUITE:
self.stat_suite_plan = self.suites.count()
self.stat_suite_run = self.suite_runlogs.count()
self.stat_suite_success = self.suite_runlogs.filter(passed=True).count()
self.stat_suite_success_rto = self.stat_suite_success / self.stat_suite_plan * 100
# 用例的 4个指标
if self.obj_type == TestBatch.OBJ_TYPE_CASE: # 在批次概念中case,可以按照多用例一同执行。
self.stat_case_plan = self.cases.count()
self.stat_case_run = self.case_runlogs.count()
self.stat_case_success = self.case_runlogs.filter(passed=True).count()
self.stat_case_success_rto = self.stat_case_success / self.stat_case_plan * 100
else: # 也可以归属与套件中被执行
# 统计用例计划数量; 先算出套件下有几个用例
cnt = 0
for suite in self.suites.all():
cnt += suite.case_suite.cases.count()
self.stat_case_plan = cnt
# 用例运行数量
cnt = 0
for slog in self.suite_runlogs.all():
cnt += slog.case_runlogs.count()
self.stat_case_run = cnt
# 用例通过
cnt = 0
for slog in self.suite_runlogs.all():
cnt += slog.case_runlogs.filter(passed=True).count()
self.stat_case_success = cnt
# 用例通过率
self.stat_case_success_rto = self.stat_case_success / self.stat_case_plan * 100
# 接口的4个值
if self.obj_type == TestBatch.OBJ_TYPE_CASE:
# 1、有几个用例 2、每个用例下有几个接口
# 接口 计划数量
cnt = 0
for tb_case in self.cases.all():
cnt += tb_case.case.case_apidefs.count()
self.stat_api_plan = cnt
# 接口 运行数量
cnt = 0
for clog in self.case_runlogs.all():
cnt += clog.case_api_logs.count()
self.stat_api_run = cnt
# 接口 成功数量
cnt = 0
for clog in self.case_runlogs.all():
cnt += clog.case_api_logs.filter(success=True).count()
self.stat_api_success = cnt
# 接口通过率
self.stat_api_success_rto = self.stat_api_success / self.stat_api_plan * 100
else: # 在套件中执行
# 接口 计划数量
cnt = 0
for tb_suite in self.suites.all():
for case in tb_suite.case_suite.cases.all():
cnt += case.case_apidefs.count()
self.stat_api_plan = cnt
# 接口 运行数量
cnt = 0
for slog in self.suite_runlogs.all():
for clog in slog.case_runlogs.all():
cnt += clog.case_api_logs.count()
self.stat_api_run = cnt
# 接口 成功数量
cnt = 0
for slog in self.suite_runlogs.all():
for clog in slog.case_runlogs.all():
cnt += clog.case_api_logs.filter(success=True).count()
self.stat_api_success = cnt
# 接口通过率
self.stat_api_success_rto = self.stat_api_success / self.stat_api_plan * 100