代码改变世界

自研测试框架ktest介绍(适用于UI和API)

2019-10-01 08:03  zouhui  阅读(1062)  评论(0编辑  收藏  举报

iTesting,爱测试,爱分享

在自动化测试的过程中,测试框架是我们绕不过去的一个工具,无论你是不需要写代码直接改动数据生成脚本,还是你需要检查测试结果甚至持续集成,测试框架都在发挥它的作用。

不同编程语言的实现出来的框架也不尽相同,但是思想总是相通的,比如尽量使框架使用者只关注自己的业务,框架帮助处理错误截图,保存错误log,出错重试甚至跟jenkins持续集成等。

可以说,一个还算合格的测试框架,可以大大提升测试效率;一个优秀的测试框架,说它能把测试人员从繁缛复杂的跟业务无关但又不得不做的工作中解脱出来也不为过。既然框架的作用这么明显,那么有哪些优秀的测试框架可以给我们用呢?

大家都知道, java里有TestNG, python里有unittest,pytest等优秀的“官方”框架, 我对python比较熟悉,之前也介绍过很多这方面的文章,相信大家也多少看过一两篇。

那么,为什么还要自己写一个框架呢?

相信常做自动化的都有自己的感悟, 比如自己业务没那么复杂,官方框架显得太重了;比如官方框架依赖很多的第三方库,每个要单独安装,更新,而且每个都有自己的用法,学习成本高;

痛点:

对于常用python写自动化的来说,unittest本身很优秀,但是不支持并发,而且测试报告支持的也不好,也不支持数据驱动。

解决方案:

基于上述痛点,大部分同学选择在原有开源框架上二次开发,比如unittest集合多线程threading.Thread实现并发,加入HTMLTestRunner实现报告展示,引入ddt实现数据驱动,然后美其名曰,我写了个框架,我每次听到都是先很崇拜,然后晚上暗搓搓拉下代码一看,MD,这不是就是搭了个积木取名要你命3000吗?你还不能说人家说自己写了一个框架不对,何况这个框架通常也很好用。

再有,开源的框架,毕竟普适多于贴合,跟自己的业务有时候就不那么紧密,为了使用某个具体功能还得引入很大一个包,也不是非常方便,另外最关键的一点是,我总觉得自己还行,想站起来试试 :)

框架是什么?我是怎么考虑框架的?

之前分享过几篇文章,

 

 这些都是我工作的一些感悟,和对框架的一些思考,可以看到思想也是循序渐进的。你也可以看到不是“官方”框架,就是二次开发的“官方”框架。 当然也可以说我是常怀觊觎之心,不停研究的目的就是我个人非常想有一套完全自己实现的框架。

什么是ktest?

一句话:

ktest is a common test framework support for Both UI and API test with run in parallel ability。

跟其它的框架有什么不同?

参考了谁?

我当然不是闭门造车,参考了unittest,pytest,ddt,还有自己公司的官方框架, 读了部分源码,研究了下部分功能实现原理。

实现的功能:

1.多线程并发。(整套框架代码没出现任何哪怕一句threading,实现了并发,神奇不,嘿嘿)

详细介绍

先不介绍技术细节, 先把自己放在一个业务测试,或者刚接触自动化脚本的测试角色上,我拿到了一个测试框架,我最先想到的是什么? 如何用对吧? 用这个框架,我原有的测试用例需要做哪些改变?这个框架有哪些方便?你对框架的期待有哪些?

下面就详细介绍:

安装:
1#目前部署在自己公司的pypi库上,后期会上传到pypi。
2pip install ktest --index-url http://jenkin.xxxx.com:8081/pypi --trusted-host jenkins.xxxx.com
3#安装好后,你可以在 Python安装目录下的如下文件找到安装的包\Lib\site-packages
你的项目应该包括哪些:

在讲用法前,我们先来直观看下,你的项目目应该是什么样子的

你的项目中应该包含:

640?wx_fmt=png

1.pages package, 这里面放你所有待测试页面,每个页面作为一个page object来保存。

可以看到,你只需要把精力放在你本身的业务上就好了。

ktest框架组成

640?wx_fmt=jpeg

package建立好了,我的测试用例,及我的待测页面要如何组织才能接入框架呢? 别急,我们先来看看框架本身长什么样子。

功能列表

看用红框标记起来的部分:

Common – 框架精华

1.欢迎关注公众号iTesting,跟万人测试团一起成长。

utiilites -- 拿来即用测试套件

这里面放一些半成品,比如连接数据库脚本,比如用excel做数据驱动,对excel进行读写的脚本。 用户不需要操心连接的建立,销毁等。

main.py   用来运行框架,并发运行控制也是在这里。并发我“舍弃”了threading.Thread, 代码量下降一半以上,且不用操心锁。
setup.py  用来把框架打包。

集成你的项目

 1#bai_du.py
 2#coding=utf-8
 3__author__ = 'iTesting'
 4import time
 5from common.abstract_base_page import AbstractBasePage
 6from page_objects import page_element
 7class BAI_DU(AbstractBasePage):
 8    BAI_DU_HOME = "http://www.baidu.com"
 9    SEARCH_DIALOG_ID = "kw"
10    SEARCH_ICON_ID = "su"
11    MEMBER_COUNT_XPATH = "//option[@value='10']"
12    SAVE_XPATH = ".//*[@id='save']"
13    search_input_dialog = page_element(id_=SEARCH_DIALOG_ID)
14    search_icon = page_element(id_=SEARCH_ICON_ID)
15    member_count = page_element(xpath=MEMBER_COUNT_XPATH)
16    save_button = page_element(xpath=SAVE_XPATH)
17    def __init__(self, browser):
18        self.browser = browser
19        AbstractBasePage.__init__(self, browser)
20    def is_target_page(self):
21        self.browser.get(self.BAI_DU_HOME)
22        print(self.is_element_displayed_on_page(self.search_icon))
23        return self.is_element_displayed_on_page(self.search_icon)
24    def search(self, search_string):
25        self.search_input_dialog.send_keys(search_string)
26        self.search_icon.click()
27        time.sleep(5)
28    def preferences(self):
29        browser = self.browser
30        browser.get(self.BAI_DU_HOME + "/gaoji/preferences.html")
31        self.member_count.click()
32        time.sleep(1)
33        self.save_button.click()
34        time.sleep(1)
35        browser.switch_to_alert().accept()

每一个你的测试类(待测页面)你需要:

你的tests package下测试用例定义

 1# test_baidu.py
 2#coding=utf-8
 3from common.selenium_helper import SeleniumHelper
 4from common.test_case_finder import data_provider
 5from common.test_decorator import TestClass, SetUpClass, Test, TearDownClass
 6from pages.baidu import BAI_DU
 7@TestClass(group='smoke', enabled=True)
 8class TestBaidu:
 9    test_case_id = '3'
10    @SetUpClass()
11    def before(self):
12        pass
13    def setUp(self):
14        self.browser = SeleniumHelper.open_browser('firefox')
15        self.baidu = BAI_DU(self.browser)
16    @data_provider([('baidu',), ('google',)])
17    @Test()
18    def test_baidu_search(self, x):
19        self.tag= 'tt'
20        """Test search"""
21        self.baidu.search(x)
22        assert 1== 1
23    @Test()
24    def test_baidu_set(self):
25        """Test set preference"""
26        print('NONONO')
27        self.baidu.preferences()
28        assert 1==0
29    def tearDown(self):
30        self.browser.delete_all_cookies()
31        self.browser.close()
32    @TearDownClass()
33    def after(self):
34        pass

注意事项

2.tags: 是每个用例的tag,定义在类属性里,test_finder查找时会解析这个tags,可以是str或者list或者tuple,或者是3者的嵌套。 框架最终会解析成一个list。

3.@SetUpClass(), @TearDownClass() 测试类装饰器,无输入参数。 每个测试类,不管它有多少个测试用例,这两个装饰器装饰的函数只会被执行一次。 一般用作测试类公用的数据的初始化,比如说,连接db查找某些值。 请注意, 并发运行,不要在这个函数里初始化你的browser,会有共享问题。

4.@TestClass(), 测试类的装饰类, 函数接受两个个参数一个是group,就是测试类所属的group,一个是enabled,默认值True。 值为False时, test_finder会把这个测试类略过。

5.@Test(), 测试装饰类, 函数接受一个参数enabled,默认值True。 值为False时, test_finder会把这个测试函数略过。

6.@data_provider(), 数据驱动装饰器。 接受一个参数,且此参数必须要iterable. 因为是数据驱动,不太可能只有一个数据,所以这个iterable,我通常我会定义成一个tuple,如果 有多个就是多个tuple, 例如[(1,2,3),(4,5,6)]这种,(1,2,3)会被解析成一条测试数据, (4,5,6)会被解析成另外一条。 这个概念来自ddt,我之前也介绍过相关框架。

7.@setUP(), @tearDown().两个函数,每个测试类必须定义,否则运行时框架会报错。 用作每个测试类的测试函数即每一条测试用例的运行前初始化和运行后的清理。

测试函数,就是以@Test()装饰的函数,一般是你的业务代码,你需要自己实现业务流程的操作和断言。如果用到setUpClass或者setUp里的方法属性,你只需要在这些属性前加self.

以上基本参照了pytest和unittest的用法,主要初衷也是为了减少迁移成本。

好,我测试类,测试函数都写好了,如何跑呢?

可用参数
1#最简单在命令行里输入ktest 即可, 框架会自动查询所有你项目文件下tests文件夹的测试用例。
2#ktest还支持如下参数:
3usage: ktest [-h]
4             [-t test     targets, should be either folder or .py file, this should be the root folder of your test cases or .py file of your test classes.]
5             [-i user provided tags,     string only, separate by comma without an spacing among all tags. if any user provided     tags are defined in test class, the test class will be considered to run. | -ai user provided tags,    string only, separate by comma without an spacing among all tags.    only all of user provided tags are defined in test class, the test class will be considered to run.]
6             [-e user provided tags,     string only, separate by comma without an spacing among all tags,if any user provided     tags are defined in test class, the test class will be Excluded. | -ae user provided tags,    string only, separate by comma without an spacing among all tags.    all the provided tags must defined in test class as well.]
7             [-I include groups,     string only, separated by comma for each tag.    if any user provided groups are defined in decorator on top of test class, the test class will be considered to run. | -AI exclude groups,     string only, separated by comma for each tag.    all user provided groups are defined in decorator on top of test class, the test class which match will be excluded to run.]
8             [-E exclude groups,    defined in decorator on top of test class, string only, separated by comma for each tag.     If any user provided groups are defined in decorator on top of test class, the test class will be Excluded to run | -AE exclude groups,    defined in decorator on top of test class, string only, separated by comma for each tag.     All user provided groups must defined in decorator on top of test class. the test class which matched will be Excluded to run]
9             [-n int number] [-r dir]

其中:

1-t 是你要运行的测试目标的根目录,默认是项目下的tests文件夹。
2-i 是测试类里定义的tags。 tags会被解析成list,用户指定的任何tag只要包含在这个lists里,并且这个测试函数所属的TestClass()是enabled和这个测试函数的enabled是True,就表示这个测试类的这个测试函数会被test_filder找到。
3-I 是装饰测试类的@TestClass()定义的group,包含两个参数, 符合用户指定的group并enabled, 那么它装饰的类会被当作一个测试类被test_finder找到。
4-e 是测试类里定义的tags。 tags会被解析成list。 用户指定的任何tags包含在list里,这个测试函数就会被test_finder忽略。
5-E 是测试类里定义的tags。 tags会被解析成list。 用户指定的任何tags包含在list里,这个测试函数就会被test_finder忽略。
6-n 并发执行的个数,默认是cpu_count。
7-r 错误重跑, 默认是True。重跑再错误,所有跟case相关的log和screenshot会被记录。

Note:

有的同学会问了,我希望跑同时包括test和regression在内这两个tags的用例呢? 谁提出的这个需求?我真想指着你的鼻子说:

 1# 定义了tag和group的更加严格版。只有用户输入的参数全部包含在测试用例定义的tags里,这个测试用例才会被test_finder扫描到。
 2  -ai user provided tags,    string only, separate by comma without an spacing among all tags.    only all of user provided tags are defined in test class, the test class will be considered to run.
 3                        Select test cases to run by tags, separated by comma,
 4                        no blank space among tag values. all user provided
 5                        tags must defined in test class as well. tags are
 6                        defined in test class with name <tags>. eg: tags =
 7                        ['smoke', 'regression']. tags value in test class can
 8                        be string, tuple or list.
 9
10  -ae user provided tags,    string only, separate by comma without an spacing among all tags.    all the provided tags must defined in test class as well.
11                        Exclude test cases by tags, separated by comma, no
12                        blank space among tag values. all of the tags user
13                        provided must in test class as well. tags are defined
14                        in test class with name <tags>. eg: tags = ['smoke',
15                        'regression']. tags value in test class can be string,
16                        tuple or list.
17
18  -AI exclude groups,     string only, separated by comma for each tag.    all user provided groups are defined in decorator on top of test class, the test class which match will be excluded to run.
19                        Select test cases belong to groups, separated by
20                        comma, no blank space among group values. All user
21                        provided group must defined in decorator on top of
22                        test class, the test class which match will be
23                        collected. groups are defined in decorator on top of
24                        test class. eg: group= 'UI'.
25
26  -AE exclude groups,    defined in decorator on top of test class, string only, separated by comma for each tag.     All user provided groups must defined in decorator on top of test class. the test class which matched will be Excluded to run
27                        Exclude test cases belong to groups, separated by
28                        comma, no blank space among group values. All of
29                        groups user provided must defined in decorator on top
30                        of test class as well. groups are defined in decorator
31                        on top of test class. eg: group= 'UI'.

事实上, 为避免用法繁琐及方便用户, -i 和-ai, -e和-ae, -I和-AI, -E和-AE 是两两互斥的, 你只能指定其中的一个。

测试报告

下面我们看下一个运行实例

1ktest -I group -n 10 -r True
2执行中console的输出:
执行中console的输出:

640?wx_fmt=jpeg

执行成功后报告的展示:

report会自动生成在你项目根目录下,以运行时时间戳为文件夹,每个测试用例一个子文件夹。

640?wx_fmt=jpeg

测试报告加入了run pass, run fail, run error的图表。 run fail代表真正的fail, run error代表代码有问题或者环境问题导致的错误。同样报告直接按照测试类filter。

640?wx_fmt=jpeg

后记:

到此为止,ktest基本成型,也能根据需求完成web UI自动化和API自动化的工作了,不同无非是你在setUP初始化你的driver时候初始化的是你的browser还是request.session. 如果你想实现分布式并发,也可以在setUP initial selenium Grid, 前提是配置好selenium-server。

还是有一些感悟:

1.框架真不是一蹴而就的,是逐渐演化的。 最后成型的这一版跟我初始的规划还是有很大差距,有些代码甚至是不得已的妥协,比如我要出html报告,就要很多测试函数无关的数据收集,那么这些数据势必会侵入我的代码,结果就是我返回的测试函数数据结构很不简洁。

彩蛋:

放部分代码段:

test_finder部分代码

640?wx_fmt=jpeg

测试类装饰器代码

640?wx_fmt=jpeg

关于更多技术实现细节,我会重新写一篇文章介绍。更多测试框架技术分享,请往下拉。


 -  End  -

作者:

Kevin Cai, 江湖人称蔡老师。

两性情感专家,非著名测试开发。

技术路线的坚定支持者,始终相信Nobody can be somebody。      

640?wx_fmt=png

                     

· 猜你喜欢的文章 ·

640?wx_fmt=gif

640?wx_fmt=png