HttpRunner HttpRunner 源码剖析 - 到底数据驱动是如何实现的?

0x7C00 · 2019年04月23日 · 最后由 0x7C00 回复于 2020年02月14日 · 3444 次阅读

概述

HttpRunner(2.1.1) 是一个非常优秀的接口自动化框架,很多公司展开接口自动化测试落地方面,会参考 HttpRunner。例如我司(但可能不考虑完全使用,会借鉴一些设计😓)。

既然考虑借鉴,就想深入源码研究。
一个核心疑问是 HttpRunner 是如何在没有具体测试方法实现的情况下 基于 Yaml 或 Json 的数据驱动呢 ?

看下 HttpRunner 的执行方式

hrun docs/data/demo-quickstart-0.yml 有没有好奇?


源码

  1. 先分享一个小技巧,一般 Pyhton 库尤其带有命令行的库,程序的入口都是在根目录的 setup.py 文件的entry_points中,例如另外一个知名库:分布式异步任务框架 Celery,这个技巧有助于以后看知名框架源码知道程序的入口!

  2. 那我们就先看 HttpRunner 的 setup.py 的entry_points,找到 101行'hrun=httprunner.cli:main_hrun', 根据包路径可以找到 httprunner/cli.py
    这个模块下有 2 个命令行函数的实现(重点关注 main_hrun),使用的命令解析库是 Python 标准库argparse,其实建议可以使用click,因为之前在项目中也是使用 argparse 后面采用了更方便可读的click

  3. 我们继续看(这里只关心 HttpRunner 通过 YAML 文件路径实现自动化测试的方法).

def main_hrun():
    ... # 省略
    parser.add_argument(
        'testcase_paths', nargs='*',
        help="testcase file path")
    #①testcase_paths 是一个列表参数.
    ... # 省略

#②找到使用这个参数的地方,搜索下当前文件,找到83行 httprunner/cli.py
    for path in args.testcase_paths:
        runner.run(path, dot_env_path=args.dot_env_path)

#③跳到runner.run 方法,看下264行 httprunner/api.py
return self.run_path(path_or_tests, dot_env_path, mapping)

# ④跳到self.run_path方法内,看下252行httprunner/api.py
return self.run_tests(tests_mapping)
# ⑤这里边有一个tests_mapping的参数,根据变量名可以看到是一个字典,也可以跳转到
tests_mapping = loader.load_tests(path, dot_env_path) 
#大概看下具体实现,这个方法的作用就是读取YAML做一些初始化的操作,返回一个dict,你可以理解为返回了所有有关于前置(hooks),测试用例等相关的元信息,我们不再继续深究,回到看到的self.run_tests方法,跳进去看下具体实现。

#⑥看下httprunner/api.py的178行
self.exception_stage = "add tests to test suite"
test_suite = self._add_tests(parsed_testcases)

# 其中parsed_testcases是一个list,其实parse_tests方法又把数据做了进一步的处理。

#⑦我们继续跳到self._add_tests 内部看下 39行,这个方法就是实现的核心逻辑了。

# 可以看到这个方法是一个闭包函数,里边内嵌了很多的函数,我们重点看下这个self._add_tests 方法。

因为这个方法非常重要,我们单独拿出来看下 (为了方便查看,做了稍微的改变)

def _add_tests(self, testcases):

    def _add_test(test_runner, test_dict):
        def test(self):
            pass
        return test

    # 初始化了一个TestSuite的实例,相信经常使用unittest库看到这个就非常熟悉了,主要用于收集测试用例.
    test_suite = unittest.TestSuite()

    for testcase in testcases:
        config = testcase.get("config", {})
        test_runner = runner.Runner(config)

        # ⑧这一行是这个_add_tests方法的核心,大家平常用Python 知道type可以用来查看对象的类型,
        # 其实type还有另外一个作用,就是用来动态创建类,type是所有类的元类,有兴趣可以看下面 type动态创建类。

        TestSequense = type('TestSequense', (unittest.TestCase,), {})

        tests = testcase.get("teststeps", [])
        for index, test_dict in enumerate(tests):
            for times_index in range(int(test_dict.get("times", 1))):
                test_method_name = 'test_{:04}_{:03}'.format(index, times_index)

                # ⑨以下2行也是核心,是为TestSequense类附加了具体的测试方法名和测试方法实现,可以参考下面 type动态创建类。
                test_method = _add_test(test_runner, test_dict)
                setattr(TestSequense, test_method_name, test_method)

        loaded_testcase = self.test_loader.loadTestsFromTestCase(TestSequense)
        setattr(loaded_testcase, "config", config)
        setattr(loaded_testcase, "teststeps", tests)
        setattr(loaded_testcase, "runner", test_runner)

        # ⑩这一行代码就是unittest提供的把测试用例方法添加到用例集,和我们正常使用unittest测试框架写测试类和测试方法是一样的。
        #只不过 HttpRunner使用了很多Python的高级语法,如果你不熟悉的话,可能是很难看明白。
        test_suite.addTest(loaded_testcase)

    return test_suite

    # 以上⑧⑨步骤总结起来就是:使用type动态创建一个继承与unitest.TestCase的类TestSequense,然后使用setattr方法给这个类附加具体的测试方法test_method,也就是_add_test(test_runner, test_dict)返还的一个函数对象。

type 动态创建类

In [10]: class Bar:pass

In [11]: type(Bar)
Out[11]: type

In [12]: Foo = type('Foo', (), {})

In [13]: type(Foo)
Out[13]: type

# 再联想下我们平时使用unittest写测试类的写法
In [15]: class TestSequense(unittest.TestCase):
            pass

# 其实是和下面的写法含义相同
TestSequense = type('TestSequense', (unittest.TestCase,), {})



# 以下一行代码的作用是为TestSequense设置一个测试方法名test_method_name以及具体实现test_method
setattr(TestSequense, test_method_name, test_method)

# 例如我们平时使用unittest写的测试用例方法实现
In [16]: class TestDemo(unittest.TestCase):
            def test_method_name(self):
                assert 1 ==1

In [17]: TestDemo.__dict__
Out[17]:
mappingproxy({'__module__': '__main__',
              'test_method_name': <function __main__.TestDemo.test_method_name(self)>,
              '__doc__': None})

 setattr(TestSequense, test_method_name, test_method)这行代码就是实现了
'test_method_name': <function __main__.test_method>,
#这样就动态的创建了测试方法


#大概创建的过程:
In [29]: class TestFoo(unittest.TestCase):
    ...:     def test_methond_name(self):
    ...:         assert 1 == 1
    ...:         print('1 == 1')
    ...:

In [30]: TestFoo1 = type('TestFoo1', (unittest.TestCase,), {})

In [31]: def test_method_name(self):
    ...:         assert 1 == 1
    ...:         print('1 == 1')
    ...:

In [32]: setattr(TestFoo1, 'test_method_name', test_method_name)

In [33]: TestFoo.__dict__
Out[33]:
mappingproxy({'__module__': '__main__',
              'test_methond_name': <function __main__.TestFoo.test_methond_name(self)>,
              '__doc__': None})

In [34]: TestFoo1.__dict__
Out[34]:
mappingproxy({'__module__': '__main__',
              '__doc__': None,
              'test_method_name': <function __main__.test_method_name(self)>})

“““

使用type动态创建类需要传以下3个参数
class的名称-字符串 'TestSequense'
继承的父类集合Python支持多重继承如果只有一个父类别忘了tuple的单元素写法(unittest.TestCase, )
class的方法名称与函数绑定
我们看下 TestSequense = type('TestSequense', (unittest.TestCase,), {})
就是123步骤的实现
”””

可以参考使用元类

总结

以上就是 HttpRunner 能够直接执行 YAML 和 JSON 文件的原理,后续的处理是对 unittest 的高级封装的使用,例如执行结果的收集 TestResult,测试用例的执行 unittest.TextTestRunner,收集的结果注入到 html 静态文件中等等,需要你对 unittest 的源码结构非常的熟悉。

看完 HttpRunner 后,debugtalk 的设计和代码能力还是非常厉害的,一个人开发应该很费脑子!

HttpRunner 有非常多的优点。

  • 代码可读性好,基本上见文释义!
  • 感觉有些方面借鉴优秀的第三方库设计如 requests 库.
  • 使用了 Python 很多高级特性,例如魔法方法del, type, setattr 等等.
  • 可插拔的 hooks 函数.
  • 真正的数据驱动! 目前很多所谓的数据驱动,例如使用 Excel 或者 YAML 的数据驱动,其实是更高级的接口参数化,并不完全是数据驱动.
  • 支持性能测试
  • .........
如果觉得我的文章对您有用,请随意打赏。您的支持将鼓励我继续创作!
共收到 7 条回复 时间 点赞
buggg 请教一个接口测试的问题 中提及了此贴 07月10日 17:29

萌新感谢楼主帮助解析常用框架的驱动原理~

夜兔君 回复

自己了解的同时顺便分享了,不知道是不是有好多人和我一样好奇!😆

大佬,能否加下微信,有些问题想请教你

jsonxia 回复

私聊一下,微信发给我,我加你哈。最近没什么时间上 testerhome,不好意思

0x7C00 回复

感谢大佬,我的微信是:xml2797119

setattr(loaded_testcase, "config", config)
setattr(loaded_testcase, "teststeps", tests)
setattr(loaded_testcase, "runner", test_runner)

这几句代码的作用是什么

yc007 回复
# setattr 是Python中的进阶用法,setattr作用是给对象“赋值”,例如:
# 赋值
In [1]: class Bar:pass
In [2]: setattr(Bar, 'name', 'allen')
In [3]: Bar.name
Out[3]: 'allen'
# 赋对象
In [10]: b = Bar()
In [11]: def foo():
    ...:     print('hello foo')
    ...:
In [12]: setattr(b, 'foo', foo)
In [13]: b.foo()
hello foo

# 还有更多高级用法,google下。
需要 登录 后方可回复, 如果你还没有账号请点击这里 注册