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

转自:https://testerhome.com/topics/18875

HttpRunner的执行方式
hrun /Users/apple/Desktop/hrun_demo/test.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的源码结构非常的熟悉。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,496评论 6 501
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,407评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,632评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,180评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,198评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,165评论 1 299
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,052评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,910评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,324评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,542评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,711评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,424评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,017评论 3 326
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,668评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,823评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,722评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,611评论 2 353