当我们以离线脚本的形式编写了大量的自动化测试代码后,很容易发现以下常见问题:

(1)对于UI自动化,当UI层的元素发生改变,需要修改所有相关的case,工作量巨大

(2)代码难以扩展,每次想新增一个自动化case就要写新的逻辑,补充新的代码

(3)代码可读性差,代码冗余,存在大量重复代码或者逻辑相似性代码

对于问题(1),可以引入POM设计思想解决

对于问题(2),可以引入数据驱动解决

对于问题(3),可以引入关键字驱动解决

这三种设计技巧分别是什么意思,怎么使用,我们一起来一探究竟

什么是POM设计思想

POM(Page Object Model)指的是页面对象模型,是一种设计思想(网上的大量文章会称为它是一种设计模式,学过开发的小伙伴可能有疑惑,设计模式通常指单例模式工厂模式等,没听过哪里还有什么POM设计模式,为了避免误解,在本文我还是将POM称为一种设计思想)

POM设计思想将页面UI元素对象业务逻辑(定位元素 以及 操作定位后的元素)、Case测试数据等分离开来,使得代码逻辑更加清晰,复用性,可维护性更高的一种方法,普遍运用于UI自动化测试当中

通过一个小Demo来感受一下POM设计思想的好处,下面这个Demo基于unittest单元测试框架

unittest是python自带的一个单元测试框架,不仅适用于单元测试,还可用于UI自动化、接口自动化测试用例的开发与执行;此框架可以组织执行测试用例,并且提供了丰富的断言方法,判断测试用例是否执行通过,并生成测试结果

在测试类TestSeach当中定义了testSeachPage方法作为一个UI自动化case,这个case主要验证百度主页的搜索功能,其操作步骤为打开百度官网,定位到页面当中的搜索框元素,输入文字,再定位到页面当中的搜索按钮元素,进行点击按钮操作搜索,最后进行搜索结果断言

from selenium import webdriverimport timeimport unittestclass TestSeach(unittest.TestCase):def testSeachPage(self):# ======测试基础数据===================url = "http://www.baidu.com"# 搜索输入框search_input = {"id": "kw"}# 百度一下 按钮search_button = {"id": "su"}# ======测试基础数据===================# ======定位元素,操作元素以及断言=================================# 启动WebDriver,地址填写本地下载的WebDriver的路径driver = webdriver.Chrome("/Users/yangzi/Downloads/chromedriver")# 访问百度driver.get(url)# 定位元素,并进行相应操作driver.find_element("id", search_input["id"]).send_keys("测试开发学习路线通关大厂")driver.find_element("id", search_button["id"]).click()# 断言 (仅演示,下面的语句断言结果永为真) # assertEqual()为unittest内置方法self.assertEqual(url, "http://www.baidu.com", msg="input url error!")# 睡眠5秒time.sleep(5)# 释放资源, 退出浏览器driver.quit()# ======定位元素,操作元素以及断言 ============================if __name__ == '__main__':unittest.main()

在以上的Demo当中,我们把写自动化case所必须的URL,元素id的值等必要的测试case基础数据定位以及操作元素的方法,以及断言方法都写在了TestSeach测试类当中的testSeachPage方法,testSeachPage方法就充当了一个UI自动化case,但是在这个case当中各部分代码耦合在一起

单独看这段Demo代码感觉还非常简单,这是因为我们只访问了1个页面,并且定位元素以及对元素操作的次数非常少,维护这样一个简单的UI页面自动化case还非常容易

在实际情况下,我们往往要维护几十个甚至上百个页面,假设某个页面新增或者删除了一个元素,若有100个case用到了该页面,在最坏情况下,我们都得依次手动修改这100个case当中的代码逻辑,这样会让我们的脚本维护变得繁琐复杂,而且变得耗时易出错

虽说IDE有相关功能可以一键修改,但采用良好的设计思想能大大提高我们的case维护效率

总的来说,与其用一个类”充胖子”,不如拆分成3个类各司其职,这三个类分别是基类、某个具体的页面类(继承基类)、测试类

接下来我们利用POM设计思想,对上面的Demo进行改写

首先定义一个基类Base Page,这是所有页面的父类。在这个基类当中定义了定位、操作定位后元素的基础方法,保证能够提供对元素进行点击赋值获取值等的能力

# page基类class Page(object):"""Page基类,所有页面page都应该继承该类"""def __init__(self, driver, base_url="https://www.baidu.com"):self.driver = driverself.base_url = base_urldef find_element(self, *loc):return self.driver.find_element(*loc)def get_attribute(self, attribute, loc):return self.find_element(*loc).get_attribute(attribute)def input_text(self, loc, text):self.find_element(*loc).send_keys(text)def click(self, loc):self.find_element(*loc).click()

接着定义某个具体的页面类(为方便说明,以下统称页面类A),页面类A继承了基类Base Page,在具体的页面类当中,可以重写或者二次封装基类当中定义的方法,使其更加易读,具备更加定制化的功能

from selenium.webdriver.common.by import Byfrom BasePage import Page# 百度搜索pageclass SearchPage(Page):# 百度搜索页面的元素信息(定位元素的方式,以及对应的值)# 搜索输入框 元素search_input = (By.ID, 'kw')# 百度一下按钮 元素search_button = (By.ID, 'su')# 百度热搜 元素hot_search = (By.XPATH, '//*[@id="1"]/div/div/div[1]')def __init__(self, driver, base_url="https://www.baidu.com"):Page.__init__(self, driver, base_url)def openBaiduHomePage(self):print("打开首页: ", self.base_url)self.driver.get(self.base_url)def input_search_text(self, text="testerGuie"):print("输入搜索关键字:测试开发Guide")self.input_text(self.search_input, text)def click_search_btn(self):print("点击 百度一下按钮")self.click(self.search_button)def get_hot_search_title(self):return self.get_attribute("title",self.hot_search)

最后就是测试类,在测试类当中创建 页面类A 的对象,调用二次封装的方法当中填入测试数据,并添加断言,从而构成了整个测试case

import unittestfrom selenium import webdriverfrom SearchPage import SearchPage# 百度搜索测试class TestSearchPage(unittest.TestCase):def setUp(self):# webdriver的path请修改为自己本地路径self.driver = webdriver.Chrome("/Users/yangzi/Downloads/chromedriver")def testSearch(self):driver = self.driverdriver.implicitly_wait(10)# 百度网址url = "https://www.baidu.com"# 搜索文本text = "测试开发进大厂"# 断言,期望验证的标题assert_title = "百度热搜"search_Page = SearchPage(driver, url)# 启动浏览器,访问百度首页search_Page.openBaiduHomePage()# 输入 搜索词search_Page.input_search_text(text)# 单击 百度一下 按钮进行搜索search_Page.click_search_btn()# 验证标题self.assertEqual(search_Page.get_hot_search_title(), assert_title)def tearDown(self):self.driver.quit()if __name__ == '__main__':unittest.main()

经过采用POM设计,相比最开始的Demo,看上去结构似乎没有多大变化,反而增加定义了额外的类,确实因为举例非常简单,但当测试case逐步增多,你会发现这样设计的好处

另外,这个测试用例还有一些能够优化的地方,比如说测试数据像网页的url、搜索的文本text、断言的期望结果等测试数据都是写死在代码当中,每新增一个case,就要在代码当中编写新的数据,我们有没有办法填写新的case时不再修改代码,减少代码冗余,只补充测试数据即可,数据驱动设计思想就能达成此目的

什么是数据驱动

本篇文章提到的数据驱动,指在软件测试领域当中的数据驱动测试(Data-Driven Testing,简称DDT)是一种软件测试方法,在数据源的帮助下重复执行相同顺序的测试步骤,测试脚本从数据源读取测试数据,而不使用硬编码值(测试数据写死在代码中)

数据源通常有以下情况:

  1. 数据硬编码在自动化测试脚本中

  2. 文件读取数据,如JsonExcelCSVYaml等格式文件

  3. 数据库中读取数据

  4. 调用接口获取数据源

  5. 本地编写数据生成的方法(脚本)

对于第1种,直接把测试数据定义在脚本中,虽然简单直观,但没有做到“测试数据”与“执行代码”解耦,不方便后期维护

对于3,4,5种情况,在自动化测试当中也会使用到,如部分参数构造,测试脚本获取其他外部数据用于脚本当中的业务逻辑等场景

在自动化测试的离线脚本当中,对于人工编写自动化case的场景,我们通常采用第2种情况,在文件当中定义自动化case的测试数据,测试脚本从文件读取数据执行自动化测试

文件读写在各大编程语言当中都提供丰富的方法支持,下面用Python举例,分别读取JsonExcelCSVYaml文件的内容,工程目录见下图

(1)读取Json文件内容

import jsondef readJSON(filename):with open(file=filename, mode="r", encoding="UTF-8") as f:res=json.load(f)return resif __name__ == '__main__':searchPageTestData = readJSON("../Case/searchPage.json")print(searchPageTestData["search"]["search_url"],searchPageTestData["search"]["search_text"],searchPageTestData["search"]["assert_title"])

(2)读取Excel文件内容

import xlrd# 读取行def readExcelByRow():lists = []book = xlrd.open_workbook("../Case/searchPage.xls")# 打开Excel文件,存在book里(不用一定叫book,改成别的名字也可以,但是要记得下面一行的book也要替换)sheet = book.sheet_by_index(0)# 索引是0的是第一张表for item in range(1, sheet.nrows):# 按行读取,for循环就是输出所要求的行lists.append(sheet.row_values(item))# 转换为表格形式return lists# 读取列def readExcelByCol():lists = []book = xlrd.open_workbook("../Case/searchPage.xls")sheet = book.sheet_by_index(0)for item in range(1, sheet.ncols):# 按列读取lists.append(sheet.col_values(item))return listsif __name__ == '__main__':rows = readExcelByRow()cols = readExcelByCol()print("Excel文件按行遍历 rows", rows)print("Excel文件按列遍历 cols", cols)

(3)读取CSV文件内容

import csvdef readCsvList():lists = []# 新建一个列表,将要提取的数据放到这个新列表里with open(file="../Case/searchPage.csv", mode="r", encoding="UTF-8") as f:reader = csv.reader(f)# 读取列表样式.reader()next(reader)# 不读取第一行casename,search_url,search_text,assert_titlefor item in reader:lists.append(item)return listsif __name__ == '__main__':res = readCsvList()print(res)

(4)读取Yaml文件内容

import yamldef readYaml():with open(file="../Case/searchPage.yaml", mode="r", encoding="UTF-8") as f:return yaml.safe_load(f)if __name__ == '__main__':searchPage = readYaml()print("返回数据类型", type(readYaml()))print(searchPage["search"]["search_url"])print(searchPage["search"]["search_text"])print(searchPage["search"]["assert_title"])

最后以读取Json文件为例,改写测试类的内容,测试数据url、搜索文本text、断言的标题均从Json文件当中获取

import unittestfrom selenium import webdriverfrom POM.SearchPage import SearchPagefrom Utils.readJson import readJSON# 百度搜索测试class TestSearchPage(unittest.TestCase):def setUp(self):self.driver = webdriver.Chrome("/Users/yangzi/Downloads/chromedriver")def testSearch(self):driver = self.driverdriver.implicitly_wait(10)# 从Json文件获取测试case数据searchPageTestData = readJSON("../Case/searchPage.json")# 百度网址url = searchPageTestData["search"]["search_url"]# 搜索文本text = searchPageTestData["search"]["search_text"]# 断言,期望验证的标题assert_title = searchPageTestData["search"]["assert_title"]search_Page = SearchPage(driver, url)# 启动浏览器,访问百度首页search_Page.openBaiduHomePage()# 输入 搜索词search_Page.input_search_text(text)# 单击 百度一下 按钮进行搜索search_Page.click_search_btn()# 验证标题self.assertEqual(search_Page.get_hot_search_title(), assert_title)def tearDown(self):self.driver.quit()if __name__ == '__main__':unittest.main()

采用数据驱动思想后,后续新增case只需要在Json文件补充测试数据即可,其他文件格式同理

{"search": {"search_url": "https://www.baidu.com" ,"search_text": "测试开发进大厂","assert_title":"百度热搜"},"search_02": {"search_url": "xxxx" ,"search_text": "xxxx","assert_title":"xxxx"},"search_03": {"search_url": "yyyy" ,"search_text": "yyy","assert_title":"yyyy"}}

目前部分自动化测试框架也自带了的数据驱动功能:

  • Unitest:使用装饰器@ddt装饰测试类,@data或者@file_data来装饰需要驱动的测试方法,可实现数据驱动

  • Pytest:使用装饰器@pytest.mark.parametrize("xx")

  • TestNG:使用注解@DataProvider

  • Junit:使用注解@ParameterizedTest+@ValueSource

值得关注的一点,自动化测试框架自带的数据驱动功能,也是将测试数据写在代码当中,但通过装饰器或者注解的形式完成了代码和测试数据解耦,大家可以根据自己的情况灵活选择数据驱动的方式

什么是关键字驱动

在前面的POM设计,在具体的UI页面类当中,二次封装Base Page Class 基类当中的方法,使得更加易读,提现了封装的思想,而关键字驱动实质还是体现封装的思想

关键字驱动是指将所有case依赖的公共步骤,进行再次封装,形成关键字,调用不同的关键字组合实现不同的业务逻辑,从而驱动测试用例执行

关键字驱动的实现方法一般有两种:(1)第一种:自己手动实现关键字,进行公共步骤的二次封装 (2)第二种:某些自动化测试框架已经自带关键字功能,可直接使用或者扩展自定义关键字

手动实现关键字

还是以上面搜索页面UI自动化测试Case为例,对于其他所有要用到百度页面搜索功能的case,都需要调用三个公共方法,分别是打开百度搜索openBaiduHomePage方法,输入搜索内容input_search_text(text)方法,点击搜索按钮click_search_btn()方法

可以将这三个公共方法,封装为一个关键字

经过关键字驱动改写后的测试case如下

import unittestfrom selenium import webdriverfrom POM.SearchPage import SearchPagefrom Utils.readJson import readJSON# 百度搜索测试class TestSearchPage(unittest.TestCase):def setUp(self):self.driver = webdriver.Chrome("/Users/yangzi/Downloads/chromedriver")def testSearch(self):driver = self.driverdriver.implicitly_wait(10)searchPageTestData = readJSON("../Case/searchPage.json")# 百度网址url = searchPageTestData["search"]["search_url"]# 搜索文本text = searchPageTestData["search"]["search_text"]# 断言,期望验证的标题assert_title = searchPageTestData["search"]["assert_title"]search_Page = SearchPage(driver, url)# 调用搜索关键字,完成搜索功能search_Page.search_keyword(text)# 验证标题self.assertEqual(search_Page.get_hot_search_title(), assert_title)def tearDown(self):self.driver.quit()if __name__ == '__main__':unittest.main()

还可以进一步进行优化降低业务测试人员编写自动化测试case的门槛,将关键字驱动和数据驱动进行结合,把关键字定义在Excel或CSV外部文件当中,读取文件当中的关键字,利用反射机制执行关键字的方法,各大编程语言均支持反射机制,对于Java的反射,可以使用invoke方法执行关键字方法

感兴趣可以阅读文末推荐的文章《自动化测试关键字驱动的原理及实现》Java和Python实现版本

自动化测试框架自带关键字

Robot Framework是基于关键字驱动的自动化测试框架,RF框架本身利用Python实现,已集成定义关键字功能

在引入RF框架的robot依赖库后的内部文件robot/api/deco.py可以看到@keyword('key name')装饰器的定义,从注释中可以看到@keyword的使用举例,详细用法可以查看

def keyword(name=None, tags=(), types=()):"""Decorator to set custom name, tags and argument types to keywords.This decorator creates ``robot_name``, ``robot_tags`` and ``robot_types``attributes on the decorated keyword function or method based on theprovided arguments. Robot Framework checks them to determine the keyword'sname, tags, and argument types, respectively.Name must be given as a string, tags as a list of strings, and typeseither as a dictionary mapping argument names to types or as a listof types mapped to arguments based on position. It is OK to specify typesonly to some arguments, and setting ``types`` to ``None`` disables typeconversion altogether.If the automatic keyword discovery has been disabled with the:func:`library` decorator or by setting the ``ROBOT_AUTO_KEYWORDS``attribute to a false value, this decorator is needed to mark functionsor methods keywords.Examples::@keyworddef example():# ...@keyword('Login as user "${user}" with password "${password}"', tags=['custom name', 'embedded arguments', 'tags'])def login(user, password):# ...@keyword(types={'length': int, 'case_insensitive': bool})def types_as_dict(length, case_insensitive):# ...@keyword(types=[int, bool])def types_as_list(length, case_insensitive):# ...@keyword(types=None])def no_conversion(length, case_insensitive=False):# ..."""if inspect.isroutine(name):return keyword()(name)def decorator(func):func.robot_name = namefunc.robot_tags = tagsfunc.robot_types = typesreturn funcreturn decorator

结束语

本文介绍了自动化测试当中常用的三大技巧,PO设计思想,数据驱动及关键字驱动,大家可以在实际工作当中单独使用某种设计,也可以将三者组合起来使用,减少在量级过大的自动化Case维护成本

最后感谢每一个认真阅读我文章的人,礼尚往来总是要有的,虽然不是什么很值钱的东西,如果你用得到的话可以直接拿走:

这些资料,对于【软件测试】的朋友来说应该是最全面最完整的备战仓库,这个仓库也陪伴上万个测试工程师们走过最艰难的路程,希望也能帮助到你!

下方进去可以自行获取一份完整的软件测试视频教程,我也曾靠它涨薪【保证100%免费】