痛点
UI自动化一般都是基于POM
的设计模式,将页面抽象为一个对象,而页面提供的操作方法封装为页面类的实例方法。
最开始封装页面时,大概是这样封装的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51
| import os
import datetime import time from selenium.webdriver.common.by import By
from common.base_page import BasePage from common.getconfig import conf
class TaskPage(BasePage): url = conf.get_str("env", "url") + conf.get_str("task_url", "inspection") add_locator = (By.XPATH, "//span[text()='添加']/parent::button") add_btn_locator = (By.XPATH, "//ul[contains(@class,'menu-vertical')]/li[1]") start_time_locator = (By.XPATH, "//span[@id='startTime']//input") input_time_locator = (By.XPATH, "//input[@class='ant-calendar-input ']") confirm_btn_locator = (By.XPATH, "//a[@class='ant-calendar-ok-btn']") end_time_locator = (By.XPATH, "//span[@id='endTime']//input") add_person_btn_locator = (By.XPATH, "//div[@id='UserSelect']/button") first_person_locator = (By.XPATH, "//div[@id='UserSelectTable']//tbody/tr[1]//input") second_person_locator = (By.XPATH, "//div[@id='UserSelectTable']//tbody/tr[2]//input") first_task_name_locator = (By.XPATH, "//tbody/tr[1]//a") def open_task_page(self): """打开任务管理页面""" return self.driver.get(self.url) def add_task(self): self.ac_hover(self.get_element(add_locator)) self.wait_element_clickable(add_btn_locator).click() self.get_element(start_time_locator) self.get_element(input_time_locator).send_keys('2022-02-02') self.get_element(end_time_locator) self.get_element(input_time_locator).send_keys('2022-02-03') self.get_element(add_person_btn_locator).click() self.get_element(first_person_locator).click() self.get_element(second_person_locator).click() self.get_element(confirm_btn_locator).click() time.sleep(1) return self.get_element(first_task_name_locator)
|
该页面所有的locator
作为页面类的类属性,但是如果该页面方法有很多,有很多locator
,全部写成类属性会非常臃肿,当locator
发生改变,维护起来也是费劲的。
那么,我们可以将locator
完全抽离出去,放在yaml
中或者封装成一个类,需要的时候直接调用即可,这样可以让页面类减负不少。
但是,还是有个问题,就是一个复杂的页面操作逻辑,可能操作步骤会很多,上面的例子有10个操作步骤,有的甚至有几十个,而且观察可以发现,其实每个步骤无非就是获取元素、点击、输入、悬停等操作,所以重复代码是非常多的。
那么,针对这个痛点,如何解决呢?
解决方案
从Robot Framwork
框架的关键字驱动中可以找到一些灵感。既然locator
可以抽离出去,而测试步骤(即操作步骤)重复性非常高,我们是不是也可以考虑将其抽离出去?
我们将操作步骤从代码描述的方式转化为用yaml描述,将所有操作步骤的动作给一个关键字,比如点击-click,输入-send,悬停-hover等,每一步的描述信息包括:locator
、动作、输入值(针对输入动作)、等待时间等,比如讲上面的代码转化为yaml描述:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62
| add_task:
- by: "xpath" locator: "//span[text()='添加']/parent::button" action: ac_hover
- by: "xpath" locator: "//ul[contains(@class,'menu-vertical')]/li[1]" action: click
- by: "id" locator: "name" action: send value: "自动化测试" return_value:
- by: "xpath" locator: "//span[@id='startTime']//input" action: click
- by: "xpath" locator: "//input[@class='ant-calendar-input ']" action: send value: "2022-02-22"
- by: "xpath" locator: "//a[@class='ant-calendar-ok-btn']" action: click
- by: "xpath" locator: "//span[@id='endTime']//input" action: click
- by: "xpath" locator: "//input[@class='ant-calendar-input ']" action: send value: "2022-02-23"
- by: "xpath" locator: "//a[@class='ant-calendar-ok-btn']" action: click
- by: "xpath" locator: "//div[@id='UserSelect']/button" action: click
- by: "xpath" locator: "//div[@id='UserSelectTable']//tbody/tr[1]//input" action: click - by: "xpath" locator: "//div[@id='UserSelectTable']//tbody/tr[2]//input" action: click
- by: "xpath" locator: "//div[contains(@style,'width: 1200px')]//span[text()='确 定']/parent::button" action: click wait: clickable - by: "xpath" locator: "//tbody/tr[1]//a" return_value:
|
这样描述看起来测试步骤非常清晰明了,维护也很方便,那么现在最关键的问题来了,如何解析这些步骤?
我们可以在basepage
中添加一个驱动测试步骤的引擎,即用来解析yaml中描述的测试步骤,将这些步骤转化为对应的代码去执行。其实思路也很清晰,首先将yaml中对应方法的所有测试步骤拿到,这个测试步骤是一个字典嵌套列表再嵌套字典的结构,即{"add_task":[{"by":"xpath","locator":"xxx","action":"xxx",..},{},{},....]}
,add_task
就是对应的方法名,而其值就是一个列表,它的每一个元素就代表一个测试步骤,那么我们遍历这个列表,就相当于去执行每一步操作步骤,每一步又如何执行呢?通过key-value
取值,根据action
去执行对应的动作,比如action
是click
,那么就去执行self.get_element(locator).click()
这句代码。
具体代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68
| class BasePage: _params = {} ...
def steps(self, path): """解析yaml文件中的测试步骤""" with open(path, encoding='utf-8') as f: name = inspect.stack()[1].function steps = yaml.safe_load(f)[name] return self.analysis_steps(steps) def analysis_steps(self, steps: list): rvs = [] for step in steps: if "return_value" in step.keys(): if "number" in step.keys(): time.sleep(1) rv = len(self.get_elements(step["by"], step["locator"])) rvs.append(rv) elif "value" in step.keys(): rvs.append(step["value"]) elif "attribute" in step.keys(): rv = self.get_element(step["by"], step["locator"]).get_attribute(step["attribute"]) rvs.append(rv) else: rv = self.wait_element_visible(step["by"], step["locator"]).text rvs.append(rv) if "action" in step.keys(): action = step["action"] if "click" == action: if "wait" in step.keys(): if "clickable" == step["wait"]: self.wait_element_clickable(step["by"], step["locator"]).click() else: self.get_element(step["by"], step["locator"]).click() elif "send" == action: self.get_element(step["by"], step["locator"]).send_keys(step["value"]) elif "clear" == action: self.get_element(step["by"], step["locator"]).clear() elif "ac_double_click" == action: e = self.get_element(step["by"], step["locator"]) self.ac_double_click(e) elif "ac_hover" == action: e = self.get_element(step["by"], step["locator"]) self.ac_hover(e) if "sleep" in step.keys(): time.sleep(step["sleep"]) return rvs
|
通过该驱动引擎,即可完成对测试步骤的驱动,当然,这里并没有写全所有的动作解析,可以根据自己的需求自行扩展。
没完。
还有个问题,如果测试数据是动态变化的怎么传进去呢?比如上面的例子中,要输入开始时间和结束时间,上面是直接写死的,如果我获取当前日期怎么办?
处理方案是,模板引擎替换。
1 2 3 4 5 6 7 8 9 10 11
| - by: "xpath" locator: "//div[contains(@class,'left')]//input[@class='ant-calendar-input ']" action: send value: $start_time - sleep: 1
- by: "xpath" locator: "//div[contains(@class,'right')]//input[@class='ant-calendar-input ']" action: send value: $end_time
|
value
表示需要从外部传入值,$
表示需要替换的标识符。
另外,在basepage
中设置一个类属性_params={}
来接收具体的参数替换值,在页面方法中获取当前时间+2和+4小时,并添加到_params
字典中:
1 2 3 4
| t1 = datetime.datetime.now() + datetime.timedelta(hours=2) t2 = datetime.datetime.now() + datetime.timedelta(hours=4) self._params["start_time"] = t1.strftime('%Y-%m-%d %H:%M') self._params["end_time"] = t2.strftime('%Y-%m-%d %H:%M')
|
此时,_params={"start_time":"xxx","end_time":"xxxx"}
。
然后在解析引擎中加上替换步骤即可:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| class BasePage: _params = {} ...
@staticmethod def handle_template(source_data, replace_data: dict, ): """ 替换文本变量 :param source_data: :param replace_data: :return: """ res = Template(str(source_data)).safe_substitute(**replace_data) return yaml.safe_load(res) def steps(self, path): """解析yaml文件中的测试步骤""" with open(path, encoding='utf-8') as f: name = inspect.stack()[1].function steps = yaml.safe_load(f)[name] raw = json.dumps(steps) steps = self.handle_template(raw, self._params) return self.analysis_steps(self, steps)
|
改造效果
改造后:
task_page.py
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| import os
import datetime import time from selenium.webdriver.common.by import By
from common.base_page import BasePage from common.getconfig import conf
class TaskPage(BasePage): url = conf.get_str("env", "url") + conf.get_str("task_url", "inspection")
def open_task_page(self): """打开任务管理页面""" return self.driver.get(self.url) def add_task(self): t1 = datetime.datetime.now() + datetime.timedelta(hours=2) t2 = datetime.datetime.now() + datetime.timedelta(hours=4) self._params["start_time"] = t1.strftime('%Y-%m-%d %H:%M') self._params["end_time"] = t2.strftime('%Y-%m-%d %H:%M') return self.steps(self.steps_path)
|
task_page_steps.yaml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61
| add_task:
- by: "xpath" locator: "//span[text()='添加']/parent::button" action: ac_hover
- by: "xpath" locator: "//ul[contains(@class,'menu-vertical')]/li[1]" action: click
- by: "id" locator: "name" action: send value: "自动化测试" return_value:
- by: "xpath" locator: "//span[@id='startTime']//input" action: click
- by: "xpath" locator: "//input[@class='ant-calendar-input ']" action: send value: $start_time
- by: "xpath" locator: "//a[@class='ant-calendar-ok-btn']" action: click
- by: "xpath" locator: "//span[@id='endTime']//input" action: click
- by: "xpath" locator: "//input[@class='ant-calendar-input ']" action: send value: $end_time
- by: "xpath" locator: "//a[@class='ant-calendar-ok-btn']" action: click
- by: "xpath" locator: "//div[@id='UserSelect']/button" action: click
- by: "xpath" locator: "//div[@id='UserSelectTable']//tbody/tr[1]//input" action: click - by: "xpath" locator: "//div[@id='UserSelectTable']//tbody/tr[2]//input" action: click
- by: "xpath" locator: "//div[contains(@style,'width: 1200px')]//span[text()='确 定']/parent::button" action: click wait: clickable - by: "xpath" locator: "//tbody/tr[1]//a" return_value:
|
改造后,page
的具体操作方法封装,不再需要写重复冗余的代码,变得干净清爽,所有测试步骤都用yaml管理,后期维护也更加方便简单,无需关心具体代码如何实现,只需要按照手工步测试骤,转化为yaml描述即可。