阶段三-05结合项目的异步处理

用例执行的异步处理

使用之前的代码修改、结合task执行异步任务,需要特别注意的是这里是异步调用,task和run_cases_q不是在一个进程中。

按照我们之前的理解来改写;admin.class CaseAdmin

tasks.py

页面上运行一下发现报错了,queryset不能被序列化为json

需要注意的是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

接下来是用例执行返回消息的问题需要解决

之前手动调用时我们发现delay生产出的任务是 AsyncResult

在pycharm里双击shift键搜索下,点击过去查看下他的返回

看下 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')

运行后没有大问题,但是运行过程中有关于时间的警告,我们使用 start_at = 和 finish_at = 在文件中查找看下发现是之前所有关于log.XX_at的时间方法都漏写了timezone.make_aware( ),补上即可.

周期任务的实战使用

接下来我们在jango页面上创建一个周期任务,查看是否能正常触发用例套件的执行:

周期任务由celery_beat驱动

需要注意的是Arguments 的位置参数对应的是 tasks.py,比如上方任务里选择的是 run_cases,那么位置参数对应的就是 run_cases(cases_ids, user_id),如果我想要执行用例里的 用户登录、订单统计,那么只需要填写它们的id

[[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,所以在代码和逻辑上就需要优化

单独为周期任务创建新的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)

套件修改同上,完成后运行发下没有记录计划任务的名称

此时我们需要给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

run_suites_periodic run_case_periodic 加上参数。再启用周期性任务中任务即可

各类统计参数

用例/套件 通过率、成功、失败等数据的记录。这些数据的统计应该在用例/套件执行完成之后统计

举个例子:

在套件页面执行,在这一个批次执行完成后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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值