背景介绍

近期,某位同学对HttpRunner提了一个需求点

能否支持类似 unittest 中的 skip 注解,方便灵活剔除某些用例,不执行。
目前在接口测试日常构建中,会遇到一些接口开发暂时屏蔽了或者降级,导致用例执行失败;所以想当遇到这些情况的时候,能够临时剔除掉某些用例不执行;等后续恢复后,再去掉,然后恢复执行。

针对这种情况,HttpRunner的确没有直接支持。之所以说是没有直接支持,是因为在HttpRunner中存在times关键字,可以指定某个test的运行次数。

例如,如下test中指定了times为 3,那么该test就会运行 3 次。

- test:
    name: demo
    times: 3
    request: {...}
    validate: [...]

假如要实现临时屏蔽掉某些test,那么就可以将对应testtimes设置为 0。

这虽然也能勉强实现需求,但是这跟直接将临时不运行的test注释掉没什么区别,都需要对测试用例内容进行改动,使用上很是不方便。

考虑到该需求的普遍性,HttpRunner的确应该增加对该种情况的支持。

在这方面,unittest已经有了清晰的定义,有三种常用的装饰器可以控制单元测试用例是否被执行:

该功能完全满足我们的需求,因此,我们可以直接复用其概念,尝试实现同样的功能。

实现方式

目标明确了,那需要怎么实现呢?

首先,我们先看下unittest中这三个函数是怎么实现的;这三个函数定义在unittest/case.py中。

class SkipTest(Exception):
    """
    Raise this exception in a test to skip it.

    Usually you can use TestCase.skipTest() or one of the skipping decorators
    instead of raising this directly.
    """
    pass

def skip(reason):
    """
    Unconditionally skip a test.
    """
    def decorator(test_item):
        if not isinstance(test_item, (type, types.ClassType)):
            @functools.wraps(test_item)
            def skip_wrapper(*args, **kwargs):
                raise SkipTest(reason)
            test_item = skip_wrapper

        test_item.__unittest_skip__ = True
        test_item.__unittest_skip_why__ = reason
        return test_item
    return decorator

def skipIf(condition, reason):
    """
    Skip a test if the condition is true.
    """
    if condition:
        return skip(reason)
    return _id

def skipUnless(condition, reason):
    """
    Skip a test unless the condition is true.
    """
    if not condition:
        return skip(reason)
    return _id

不难看出,核心有两点:

明确了这两点之后,我们要如何在HttpRunner中实现同样的功能,思路应该就比较清晰了。

因为HttpRunner同样也是采用unittest来组织和驱动测试用例执行的,而具体的执行控制部分都是在httprunner/runner.py_run_test方法中;同时,在_run_test方法中会传入testcase_dict,也就是具体测试用例的全部信息。

那么,最简单的做法,就是在YAML/JSON测试用例中,新增skip/skipIf/skipUnless参数,然后在_run_test方法中根据参数内容来决定是否执行raise SkipTest(reason)

例如,在YAML测试用例中,我们可以按照如下形式新增skip字段,其中对应的值部分就是我们需要的reason

- test:
    name: demo
    skip: "skip this test unconditionally"
    request: {...}
    validate: [...]

接下来在_run_test方法,要处理就十分简单,只需要判断testcase_dict中是否包含skip字段,假如包含,则执行raise SkipTest(reason)即可。

def _run_test(self, testcase_dict):
    ...

    if "skip" in testcase_dict:
        skip_reason = testcase_dict["skip"]
        raise SkipTest(skip_reason)

    ...

这对于skip机制来做,完全满足需求;但对于skipIf/skipUnless,可能就会麻烦些,因为我们的用例是在YAML/JSON文本格式的文件中,没法像在unittest中执行condition那样的 Python 表达式。

嗯?谁说在YAML/JSON中就不能执行函数表达式的?在HttpRunner中,我们已经实现了该功能,即:

在此基础上,我们要实现skipIf/skipUnless就很简单了;很自然地,我们可以想到采用如下形式来进行描述。

- test:
    name: create user which existed (skip if condition)
    skipIf: ${skip_test_in_production_env()}
    request: {...}
    validate: [...]

其中,skip_test_in_production_env定义在debugtalk.py文件中。

def skip_test_in_production_env():
    """ skip this test in production environment
    """
    return os.environ["TEST_ENV"] == "PRODUCTION"

然后,在_run_test方法中,我们只需要判断testcase_dict中是否包含skipIf字段,假如包含,则将其对应的函数表达式取出,运行得到其结果,最后再根据运算结果来判断是否执行raise SkipTest(reason)。对函数表达式进行解析的方法在httprunner/context.pyexec_content_functions函数中,具体实现方式可阅读之前的文章。

def _run_test(self, testcase_dict):
    ...

    if "skip" in testcase_dict:
        skip_reason = testcase_dict["skip"]
        raise SkipTest(skip_reason)
    elif "skipIf" in testcase_dict:
        skip_if_condition = testcase_dict["skipIf"]
        if self.context.exec_content_functions(skip_if_condition):
            skip_reason = "{} evaluate to True".format(skip_if_condition)
            raise SkipTest(skip_reason)

    ...

skipUnlessskipIf类似,不再重复。

通过该种方式,我们就可以实现在不对测试用例文件做任何修改的情况下,通过外部方式(例如设定环境变量的值)就可以控制是否执行某些测试用例。

效果展示

skip/skipIf/skipUnless机制实现后,我们对测试用例的执行控制就更加灵活方便了。

例如,我们可以很容易地实现如下常见的测试场景:

更重要的是,我们无需对测试用例文件进行任何修改。

HttpRunner项目中存在一个示例文件,httprunner/tests/data/demo_testset_cli.yml,大家可以此作为参考。

在运行该测试集后,生成的测试报告如下所示。

最后做个预告,HttpRunner在中文使用文档方面一直比较缺失,近期将集中时间进行梳理,争取尽快给大家一个比较系统的文档手册。


↙↙↙阅读原文可查看相关链接,并与作者交流