痛点

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):
# 1、鼠标悬停到添加按钮上
self.ac_hover(self.get_element(add_locator))
# 2、点击添加
self.wait_element_clickable(add_btn_locator).click()
# 3、点击开始时间输入框
self.get_element(start_time_locator)
# 4、输入开始时间
self.get_element(input_time_locator).send_keys('2022-02-02')
# 5、点击结束时间输入框
self.get_element(end_time_locator)
# 6、输入结束时间
self.get_element(input_time_locator).send_keys('2022-02-03')
# 7、点击人员选择框
self.get_element(add_person_btn_locator).click()
# 8、勾选第一个人
self.get_element(first_person_locator).click()
# 9、勾选第二个人
self.get_element(second_person_locator).click()
# 10、点击确定
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:
# 1、鼠标悬停添加按键
- by: "xpath"
locator: "//span[text()='添加']/parent::button"
action: ac_hover
# 2、点击重点任务
- by: "xpath"
locator: "//ul[contains(@class,'menu-vertical')]/li[1]"
action: click
# 3、输入任务名称
- by: "id"
locator: "name"
action: send
value: "自动化测试"
return_value:
# 4、点击开始时间
- by: "xpath"
locator: "//span[@id='startTime']//input"
action: click
# 5、输入开始时间
- by: "xpath"
locator: "//input[@class='ant-calendar-input ']"
action: send
value: "2022-02-22"
# 6、点击确定
- by: "xpath"
locator: "//a[@class='ant-calendar-ok-btn']"
action: click
# 7、点击结束时间
- by: "xpath"
locator: "//span[@id='endTime']//input"
action: click
# 8、输入结束时间
- by: "xpath"
locator: "//input[@class='ant-calendar-input ']"
action: send
value: "2022-02-23"
# 9、点击确定
- by: "xpath"
locator: "//a[@class='ant-calendar-ok-btn']"
action: click
# 10、点击添加人员
- by: "xpath"
locator: "//div[@id='UserSelect']/button"
action: click
# 11、勾选前两名执行人
- by: "xpath"
locator: "//div[@id='UserSelectTable']//tbody/tr[1]//input"
action: click
- by: "xpath"
locator: "//div[@id='UserSelectTable']//tbody/tr[2]//input"
action: click
# 12、点击确定
- by: "xpath"
locator: "//div[contains(@style,'width: 1200px')]//span[text()='确 定']/parent::button"
action: click
wait: clickable
# 13、获取第一条任务名称
- 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去执行对应的动作,比如actionclick,那么就去执行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文件中的测试步骤"""
# 读取描述测试步骤的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:
# yaml中有return_value的处理方式
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)

# yaml中有action的处理方式
if "action" in step.keys():
action = step["action"]
# 动作为点击
if "click" == action:
# 步骤中是否有wait关键字,有:表明点击动作用显示等待
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)
# yaml中有sleep的处理方式
if "sleep" in step.keys():
time.sleep(step["sleep"])
return rvs

通过该驱动引擎,即可完成对测试步骤的驱动,当然,这里并没有写全所有的动作解析,可以根据自己的需求自行扩展。

没完。

还有个问题,如果测试数据是动态变化的怎么传进去呢?比如上面的例子中,要输入开始时间和结束时间,上面是直接写死的,如果我获取当前日期怎么办?

处理方案是,模板引擎替换。

1
2
3
4
5
6
7
8
9
10
11
# 6、输入开始时间
- by: "xpath"
locator: "//div[contains(@class,'left')]//input[@class='ant-calendar-input ']"
action: send
value: $start_time
- sleep: 1
# 7、输入结束时间
- 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:
# 1、鼠标悬停添加按键
- by: "xpath"
locator: "//span[text()='添加']/parent::button"
action: ac_hover
# 2、点击重点任务
- by: "xpath"
locator: "//ul[contains(@class,'menu-vertical')]/li[1]"
action: click
# 3、输入任务名称
- by: "id"
locator: "name"
action: send
value: "自动化测试"
return_value:
# 4、点击开始时间
- by: "xpath"
locator: "//span[@id='startTime']//input"
action: click
# 5、输入开始时间
- by: "xpath"
locator: "//input[@class='ant-calendar-input ']"
action: send
value: $start_time
# 6、点击确定
- by: "xpath"
locator: "//a[@class='ant-calendar-ok-btn']"
action: click
# 7、点击结束时间
- by: "xpath"
locator: "//span[@id='endTime']//input"
action: click
# 8、输入结束时间
- by: "xpath"
locator: "//input[@class='ant-calendar-input ']"
action: send
value: $end_time
# 9、点击确定
- by: "xpath"
locator: "//a[@class='ant-calendar-ok-btn']"
action: click
# 10、点击添加人员
- by: "xpath"
locator: "//div[@id='UserSelect']/button"
action: click
# 11、勾选前两名执行人
- by: "xpath"
locator: "//div[@id='UserSelectTable']//tbody/tr[1]//input"
action: click
- by: "xpath"
locator: "//div[@id='UserSelectTable']//tbody/tr[2]//input"
action: click
# 12、点击确定
- by: "xpath"
locator: "//div[contains(@style,'width: 1200px')]//span[text()='确 定']/parent::button"
action: click
wait: clickable
# 13、获取第一条任务名称
- by: "xpath"
locator: "//tbody/tr[1]//a"
return_value:

改造后,page的具体操作方法封装,不再需要写重复冗余的代码,变得干净清爽,所有测试步骤都用yaml管理,后期维护也更加方便简单,无需关心具体代码如何实现,只需要按照手工步测试骤,转化为yaml描述即可。