背景介绍

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

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

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

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

1
2
3
4
5
- test:
    name: demo
    times: 3
    request: {...}
    validate: [...]

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

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

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

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

  • @unittest.skip(reason):无条件跳过当前测试用例
  • @unittest.skipIf(condition, reason):当条件表达式的值为true时跳过当前测试用例
  • @unittest.skipUnless(condition, reason):当条件表达式的值为false时跳过当前测试用例

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

实现方式

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

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
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

不难看出,核心有两点:

  • 对于skip,只需要在该测试用例中raise SkipTest(reason),而SkipTestunittest/case.py中定义的一个异常类;
  • 对于skipIfskipUnless,相比于skip,主要是需要指定一个条件表达式(condition),然后根据该表达式的实际值来决定是否skip当前测试用例。

明确了这两点之后,我们要如何在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

1
2
3
4
5
- test:
    name: demo
    skip: "skip this test unconditionally"
    request: {...}
    validate: [...]

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

1
2
3
4
5
6
7
8
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中,我们已经实现了该功能,即:

  • debugtalk.py中定义函数,例如func(a, b)
  • YAML/JSON中通过${func(a,b)}对函数进行调用

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

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

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

1
2
3
4
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函数中,具体实现方式可阅读之前的文章。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
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机制实现后,我们对测试用例的执行控制就更加灵活方便了。

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

  • 对测试用例进行分组,P0/P1/P2等,然后根据实际需求选择执行哪些用例
  • 通过环境变量来控制是否执行某些用例

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

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

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