通用技术 allure 报告合并的粗暴解决方案 (附带 pytest 部分源码分析)

SpuerHook · 2021年03月18日 · 最后由 清水 回复于 2024年01月22日 · 6756 次阅读

哈喽啊,各位社区的老铁们

还是由于项目的原因,相同的测试用例需要在不同的环境下执行。
想通过 allure 将不同的测试结果合并到一个测试报告中。
经过两天的尝试与学习,得到了一个比较粗暴,但是有效的解决方案。

环境

pytest + allure

方法思路

当我们通过 allure generate testresult1 testresult2 -o report_dir 会得到什么呢。
如果你是相同的测试用例那么得到的报告并不会呈现出两个测试用例或测试套。
而是其中一个会存在于另一个的重试次数里。

经过反复的尝试分析,发现当测试类名不同时合并的报告才会以两个测试套的形式出现。
所以按这个思路,我们只要动态修改测试类名即可。
但是我尝试了很多并没有得到一个比较好的在 pytest 运行时修改类名的方案。

再次经过尝试后发现,allure 是根据 caseid 来区别测试类是否是一个的。
caseid 则是用 fullname(测试文件名.测试类名.测试用例名) 得到的 md5 值
那么我便选择在 allure 生成 caseid 前将依赖的测试类名修改掉。

如何动态修改测试类名呢,我的方法是用 allure.feature("") 来携带信息
测试类以 TestFoo_xxx 为格式然后用 feature 携带的信息替换到 xxx 即可

具体行动

首先

跟大家梳理一下 allure 是怎么获取 pytest 的测试信息和测试结果的。
当我们执行一个测试用例的时候,pytest 的执行测试流程主要有几个步骤
1. 插件与配置文件的初始化。
2. 收集环境信息与测试用例的信息
3. 执行测试用例
4 收集打印测试结果,退出测试
每个测试步骤都对应不同的 hook 函数,感兴趣的老铁可以去 pytest 官方教程学习。

然后

那么问题来了,allure 在哪个阶段手的信息呢?
不少老铁包括我以前会认为实在用例执行结束以后。
年轻的我们总是这么普通,又这么自信。

下面梳理一下这部分的源码,找到它!
pytest 执行用例时用到的 hook 函数
_pytest/runner.py

def pytest_runtest_protocol(item, nextitem):
    item.ihook.pytest_runtest_logstart(nodeid=item.nodeid, location=item.location)
    runtestprotocol(item, nextitem=nextitem)
    item.ihook.pytest_runtest_logfinish(nodeid=item.nodeid, location=item.location)
    return True

细心的老铁已经看出
runtestprotocol 这个函数就是执行测试用例的。
看看这个函数里面是啥

def runtestprotocol(item, log=True, nextitem=None):
    hasrequest = hasattr(item, "_request")
    if hasrequest and not item._request:
        item._initrequest()
    rep = call_and_report(item, "setup", log)
    reports = [rep]
    if rep.passed:
        if item.config.getoption("setupshow", False):
            show_test_item(item)
        if not item.config.getoption("setuponly", False):
            reports.append(call_and_report(item, "call", log))
    reports.append(call_and_report(item, "teardown", log, nextitem=nextitem))

到这里我们先走到 call_and_report 这个函数,看看这里是啥

def call_and_report(
    item, when: "Literal['setup', 'call', 'teardown']", log=True, **kwds
):
    call = call_runtest_hook(item, when, **kwds)
    hook = item.ihook
    report = hook.pytest_runtest_makereport(item=item, call=call)
    if log:
        hook.pytest_runtest_logreport(report=report)
    if check_interactive_exception(call, report):
        hook.pytest_exception_interact(node=item, call=call, report=report)
    return report

细心的老铁再次发现奥秘,setup,call,teardown 这三个参数代表了不同的执行阶段。
首先会先进行 setup,通过 call_runtest_hook 这个函数走到哪里呢。

def call_runtest_hook(item, when: "Literal['setup', 'call', 'teardown']", **kwds):
    if when == "setup":
        ihook = item.ihook.pytest_runtest_setup
    elif when == "call":
        ihook = item.ihook.pytest_runtest_call
    elif when == "teardown":
        ihook = item.ihook.pytest_runtest_teardown
    else:
        assert False, "Unhandled runtest hook case: {}".format(when)
    reraise = (Exit,)  # type: Tuple[Type[BaseException], ...]
    if not item.config.getoption("usepdb", False):
        reraise += (KeyboardInterrupt,)
    return CallInfo.from_call(
        lambda: ihook(item=item, **kwds), when=when, reraise=reraise
    )

这个函数里面就包含着 allure 收集第一次信息的函数,
它!就!是!.pytest_runtest_setup
让我赶紧看看里面有啥,为啥说是它在收集。

@pytest.hookimpl(hookwrapper=True)
    def pytest_runtest_setup(self, item):
        if not self._cache.get(item.nodeid):...

        yield

        uuid = self._cache.get(item.nodeid)
        test_result = self.allure_logger.get_test(uuid)

        os_tag = None
        for i in allure_labels(item):
            name, value = i
            if name == "feature":
                os_tag = value
                break
        test_result.name = allure_name(item, params)
        full_name = allure_full_name(item)
        nodeid = item.nodeid
        if os_tag:
            full_name = full_name.replace("xxx", os_tag)
            nodeid = nodeid.replace("xxx", os_tag)
        test_result.fullName = full_name
        test_result.historyId = md5(nodeid)
        test_result.testCaseId = md5(full_name)
        test_result.description = allure_description(item)

这里的源码已经是我修改后的了。
当我们跳到这个函数里的时候。我需要看一下它的模块名与文件路径。居然 allure/listener.py 这个模块里。
这个函数结束 allure 就收集到了一些基本的信息。
前文已经提到了 allure 是通过 caseid 区别测试用例的。
caseid 则是通过 full_name 生成。
我这里修改了 full_name 与 nodeid
至此就已经完成了。
但是测试用例这个时候并没有执行完成。
限于篇幅有限,就不再这里进行下面的分析了,感兴趣的老铁可以尝试自己去分析看看。

反思的问题

1.使用 feature 携带信息是否合理。
2.这个合并报告的问题解决两天是否得不偿失。

差不多就这些了。以上就是我的分享。
不对的地方请大家及时指正,避免误人子弟。

共收到 10 条回复 时间 点赞

也可以动态修改测试用例名,但是我感觉修改类名测试报告可能更直观一点。

我能问一下,pytest 的源码是必须要学的么?还是说等有需求时再去官网找自己需要的那部分呢?

llyyff 回复

非必须,我认为还是看个人需求,没有需求没有动力。
建议有时间和精力还是多看看源码。看多了能提升编码能力。

SpuerHook 回复

好的,多谢大佬

llyyff 回复

大佬这个称呼先留着,,等过几年再叫😁

没写过 python 不知道这样行不行。
os_arg 根据需求,可以通过环境变量或者其他方式传过来。

RFC2109 回复

应该是可行的。而且比较优雅。

SpuerHook 回复

想问一下怎么动态修改测试用例名?是用 allure.dynamic.title 吗?

想问一下楼主是在测试方法的上面加@allure.feature("xxx") 这个装饰器吗?

RFC2109 回复

正好我也碰到这个问题,然后试了一下是可行的👍

需要 登录 后方可回复, 如果你还没有账号请点击这里 注册