最近为了协助测试人员完成Selenium + Python的自动化UI测试场景落地,在他们原来面向过程的Python代码基础上做了一层封装,做成了一个面向对象的Python小工具(这也是本人第一次接触Python语言,重温从零开始学一门语言,感触良多)。
调整后的代码基本上能够满足:
可以动态读取yaml配置内容;
主程序不用反复修改;
测试用例可以根据“项目 -> 版本 -> py文件”进行定位获取;
测试用例模组能够动态引入;
会自动采用最新测试用例进行自动化测试;
测试人员只需关心测试用例编写,甚至可以使用Katalon Recorder录制用例脚本后放入测试(只需要做小量的调整);
采用HtmlTestRunnerCN库自动生成html测试报告方便通过集成Nginx直接输出成果;
autoui
|-- configs
| |-- common.yaml
| |-- config.py
| `-- paths.yaml
|-- main.py
`-- <<项目名称>>
`-- v2_5_0_0
`-- quality.py
从目录结构可以看出autoui目录下就一个main.py这个是主程序入口,接着就是configs目录,这个是配置文件存放地方是本项目固定目录。接着就是<<项目名称>>目录,这个就是需要被自动化UI测试的项目名称,而在其下方有一个v2_5_0_0这个是需要被压测的用例版本号,再往下quality.py文件就是测试用例的模组。
这套工具就像“springboot”那样“约定大于配置”,configs文件夹存放路径和名称不能改变,里面存放的东西是可以随意的。被自动化UI测试的项目文件夹一定要放在与main.py文件同级的路径下,且必须按照“项目 -> 版本号”这种方式创建文件夹,在版本号下方再存放模组文件。
另外在代码编写的时候,建议每一个module中只含一个class,所有的测试用例都以函数的方式展现,因为后面会根据这个规则统一处理做到用例全覆盖的。
import unittest
...
# 系统主类执行方法
if __name__ == '__main__':
...
# 3. 遍历获取字典内容
for params in dict_param.items():
# 4. report_date用于记录当前报告输出时间
report_date = time.strftime('%Y-%m-%d')
# 5. 将dict的key(这里的params[0])作为项目名称
project_name = params[0] + '测试报告.html'
# 6. 通过get_execute_file获取到当前项目中可以执行的样例代码py文件,而params[1]代表的文件,之后的[1]代表路径字符串所以要获取到路径则使用params[1][1]
exec_url_list = get_execute_file(params[1][1])
# 7. 如果得到的路径不为空的情况下
if len(exec_url_list) > 0:
# 8. 初始化unittest实例
autoui_instance = unittest.TestSuite()
print('>>>>>> project ' + project_name + ' has begin execute ......')
# 9. 遍历可执行文件路径集合
for exec_url in exec_url_list:
# 10. 对路径进行转换,从当前文件路径开始的包路径获取
package_url = exec_url.replace(root_url + '\\', '').replace('\\', '.').replace('.py', '')
# 11. 根据包路径动态获取该文件的模块对象
module = importlib.import_module(package_url)
# 12. 根据模块和inspect.isclass方法可以获取到当前模块中的所有类名
classes = [clsname for (clsname, fullname) in inspect.getmembers(module, inspect.isclass)]
# 13. 遍历所有类名
for clsname in classes:
# 14. 通过getattr获取当当前模块里该类名对应的类对象
clazz = getattr(module, clsname)
# 15. 根据模块和类名和inspect.isfunction方法获取到该类中的所有方法
methods = [method_name for (method_name, method) in
inspect.getmembers(getattr(module, clsname), inspect.isfunction)]
# 16. 遍历所有方法
for method in methods:
# 17. 只需要关注"ui_"关键字开头的方法将其放入unittest实力当中加入到ui自动化测试队列中
if method.find('ui_') >= 0:
autoui_instance.addTest(clazz(method))
# 18. 创建到处文件路径
export_path = report_path + "\\" + params[0] + "\\" + report_date
export_url = export_path + "\\" + project_name
if not os.path.exists(export_url):
os.makedirs(export_path)
# 19. 创建到处文件对象(若存在相同名字文件则自动覆盖)
fp = open(export_url, "wb")
# 20. 调用HTMLTestRunnerCN来到处测试报告
runner = HTMLTestRunnerCN.HTMLTestReportCN(stream=fp, title="自动化测试报告", description='详细测试用例结果', tester='梁泽良')
runner.run(autoui_instance)
上面的代码采用了类似Java中反射的方式获取对象。
首先在第10点中获取模块的路径并将其转换为包路径。接着将其通过importlib.import_module获取到指定的module对象。接着又通过遍历获取到module中的所有class,遍历所有class又获取到所有函数。
由于与测试人员约定俗成了测试用例按照函数来编写,因此在遍历函数的时候获取到“ui_”命名的函数就可以将其放入test实例里面。
至此,能够遍历统一目录下所有module中所有的类里面的所有函数,做到全覆盖(由于是Python新手,这样的写法未免过于累赘后面慢慢改进)。
import time
......
# 创建一个名为质量管理用例(quality代表质检管理,case代表用例)
class quality_case(unittest.TestCase):
@classmethod
def setUpClass(cls):
# 初始化webdriver(方便后面对浏览器进行替换)
cls.driver = webdriver.Chrome()
def setUp(self):
self.driver.maximize_window()
self.driver.implicitly_wait(10)
# 注意这里要用“ui_”开头来定义函数
def ui_case1(self):
self.driver.get(user_login)
...
self.driver.find_element_by_css_selector("[placeholder='请输入账号名']").send_keys(user_account)
...
def ui_case2(self):
self.driver.get(admin_login)
...
@classmethod
def tearDownClass(cls):
cls.driver.quit()
另外,关于用例module的写法将严格按照unittest.TestCase标准写法编写,先重写setUpClass获取到class对象,在这个函数下先对webdriver的Chrome浏览器对象进行初始化,上面的@classmethod可以理解成setUpClass函数作为quality_case类的简单工厂用于构造对象,这个同样适用于下面的tearDownClass函数。
我简单的理解为setUpClass是TestCase第一个执行函数,因此webdriver的初始化就放在这里就好了,但要想要只构造一次就可以了不用每次都构建webdriver,这样就只需要加上@classmethod就好了。
同理,tearDownClass是最后执行的函数(不管用例是否执行成功都会执行),因此将webdriver的quit函数放在这里执行就能够避免因为异常导致浏览器线程不关闭的现象了。但要获取webdriver对象也就只能通过@classmethod来获取了。
至于setUp函数就是真正执行用例前的函数,用于对测试内容的“预设”,这里就通过self获取driver对象就能够进行webdriver的设定了。
再后面就是ui_开头的自定义用例了。
至此,该讲的已经讲完。后面测试人员只需要按照TestCase的标准写法编写就可以了,才疏学浅献丑了。