一. Yaml 数据格式
应用场景
概念和语法规则
Yaml 是一种所有编程语言可用的友好的数据序列化标准。语法和其他高阶语言类似,并且可以简单表达字典、列表
和其他基本数据类型的形态。语法规则如下:
- 大小写敏感。
- 使用缩进表示层级关系。
- 缩进时不允许使用Tab键,只允许使用空格。
- 缩进的空格数目不重要,只要相同层级的元素左侧对齐即可
快速体验
将一个字典 {“name”: “xiaoming”, “age”, “18”} 写成 Yaml 的形式,并输入结果
data.yaml
name: "xiaoming"
age: "18"
demo.py
import yaml
with open("./data.yaml",'r') as f:
data = yaml.load(f)
print(data)
1.1 字典和列表
应用场景
使用的数据需要使用列表和字典展示的时候,也同时为后续复杂的数据做铺垫
1.1.1 字典
需求:
使用 Yaml 编写 {“name”: “xiaoming”, “age”, “18”
name: "xiaoming"
age: "18"
1.1.2 列表
需求:
使用 Yaml 编写 [“1”, “2”, “3”]
- "1"
- "2"
- "3"
小结
字典直接写key和value,每一个键值对占一行。
列表中的元素需要用 “-” 来表示,每一个元素前面都有一个 “-”,后面接元素内容
1.2 字典和列表相互嵌套
应用场景
真实的数据一般不会是一个单纯的字典和列表,掌握了相互的嵌套,再复杂的数据也能看懂。
1.2.1 字典嵌套字典
需求:
使用 Yaml 编写 {people1: {“name”: “xiaoming”, “age”, “18”}, people2: {“name”: “xiaohong”, “age”, “20”}}
peopel1:
name: "xiaoming"
age: "18"
peopel2:
name: "xiaohong"
age: "20"
1.2.2 字典嵌套列表
使用 Yaml 编写 {people1: [“1”, “2”, “3”]}
peopel1:
- "1"
- "2"
- "3"
1.2.3 列表嵌套字典
使用 Yaml 编写 [{“name”: “xiaoming”, “age”, “18”}, {“name”: “xiaohong”, “age”, “20”}]
-
peopel1:
name: "xiaoming"
age: "18"
-
peopel2:
name: "xiaohong"
age: "20"
1.2.4 列表嵌套列表
使用 Yaml 编写 [[“1”, “2”, “3”], [“4”, “5”, “6”]]
-
- "1"
- "2"
- "3"
-
- "4"
- "5"
- "6"
强化训练:
使用 Yaml 编写:
[“1”, “2”, {“name”: [“xiaoming”, “xiaohong”], “age”: “18”}, [{“name”: “xiaoqiang”, “age”: “28”}, “3”, “4”], “5”,[“7”, “8”]]
- "1"
- "2"
-
name:
- "xiaoming"
- "xiaohong"
age: "18"
-
-
name: "xiaoqiang"
age: "28"
- "3"
- "4"
- "5"
-
- "7"
- "8"
注意点:
pycharm会自动将yaml中的tab转化成空格。但是不能复制”tab键“,不会进行转化。同级别左侧必须要对齐,少个空格无所谓。
1.3 其他基本数据类型
应用场景
工作中的 yaml 除了最基本的字符串,还可能要使用其他的一些数据类型。学习之后,我们就可以编写包含其他数据类型的 yaml 文件了。同时,也可以知道其中部分数据类型的注意点。
1.3.1 整数
使用 Yaml 展示 整数
"整数1": 1
"整数2": 25
1.3.2 浮点数
使用 Yaml 展示 浮点数
"浮点数1": 1.8
"浮点数2": 20.6
1.3.3 布尔类型
使用 Yaml 展示 布尔类型
"布尔类型1": True
"布尔类型2": TRUE
"布尔类型3": true
"布尔类型4": False
"布尔类型5": FALSE
"布尔类型6": false
1.3.4 空值
使用 Yaml 展示 空值
"空值1": Null
"空值2": NULL
"空值3": null
1.3.5 时间
使用 Yaml 展示 时间
"时间": 2019-03-25 05:28:34.2345
1.3.6 字符串
使用 Yaml 展示 字符串
"字符串1": "hello"
"字符串2": hello
二. Yaml 在 python 中的读写
应用场景
yaml 的读写在后续的数据驱动应用中起到不可或缺的作用。掌握yaml的读写不仅可以用于测试,如果以后大家想从
事 python 的工作,也会可能会使用到。
2.1 读取 Yaml 文件
demo_load.py
import yaml
# 打开要读取的文件
with open("./data.yaml", 'r') as f:
# 加载文件内容
data = yaml.load(f)
# 打印结果
print(data)
小结
读取实际上就是先使用 with open 的形式获取文件对象,再使用 yaml 中的 load 方法加载这个文件对象
注意点
在 windows 系统中,去读中文可能有问题,需要在 with open 中增加参数 encoding=‘utf-8’
import yaml
# 打开要读取的文件
with open("./data.yaml", 'r', encoding='utf-8') as f:
# 加载文件内容
data = yaml.load(f)
# 打印结果
print(data)
2.2 写入 Yaml 文件
demo_dump.py
import yaml
# 准备data数据
data = {'search_data': {'search_test_002': {'expect': {'value': 'hello'}, 'value': 'hello'},
'search_test_001': {'expect': [4, 5, 6], 'value': 456}}}
# 将data写入
with open("./data_dump.yaml", "w") as f:
yaml.dump(data, f)
data_dump.yaml
search_data:
search_test_001:
expect: [4, 5, 6]
value: 456
search_test_002:
expect: {value: hello}
value: hello
问题
我们会发现在写入的时候,如果将内容换成中文,那么中文的显示是有问题的。此时我们应该设置一下写入的编码。
解决方案
demo_dump.py
import yaml
# 准备data数据
data = {search_data': {'search_test_002': {'expect': {'value': '你好'}, 'value': '你好'},
'search_test_001': {'expect': [4, 5, 6], 'value': 456}}}
# 将data写入
with open("./data_dump.yaml", "w") as f:
yaml.dump(data, f, encoding='utf-8', allow_unicode=True)
小结
写入实际上就是先使用 with open 的形式获取文件对象,再使用 yaml 中的 dump 方法加载数据和这个文件对象
三. Yaml 数据驱动应用
所谓数据驱动应用,就是将我们在自动化测试脚本的使用的数据部分不在脚本中 “写死” 而是通过yaml的形式进行展现。以后如果遇到对数据需求的更变,也可以快速的进行修改。
3.1 项目需求
项目需求
使用 pytest 框架 + po 模式 + yaml 数据 完成通讯里添加联系人案例。
3.2 项目准备
使用 pytest 框架、po 模式 打开要测试的通讯里程序
pytest.ini
[pytest]
addopts = -s --reruns 0
testpaths = ./scripts
python_files = test_*.py
python_classes = Test*
python_functions = test_*
page/new_contact_page.py
from selenium.webdriver.common.by import By
from base.base_action import BaseAction
class NewContactPage(BaseAction):
# 姓名输入框
name_edit_text = By.XPATH, "//*[@text='姓名']"
# 电话输入框
phone_edit_text = By.XPATH, "//*[@text='电话']"
# 输入姓名
def input_name(self, text):
self.input(self.name_edit_text, text)
# 输入电话
def input_phone(self, text):
self.input(self.phone_edit_text, text)
page/contact_list_page.py
from selenium.webdriver.common.by import By
from base.base_action import BaseAction
class ContactListPage(BaseAction):
# 添加联系人
add_contact_button = By.ID, "com.android.contacts:id/floating_action_button"
# 点击添加联系人
def click_add_contact(self):
self.click(self.add_contact_button)
page/page.py
from page.contact_list_page import ContactListPage
from page.new_contact_page import NewContactPage
class Page:
def __init__(self, driver):
self.driver = driver
@property
def contact_list(self):
return ContactListPage(self.driver)
@property
def new_contact(self):
return NewContactPage(self.driver)
scripts/test_contact.py
import time
import pytest
from base.base_driver import init_driver
from page.page import Page
class TestContact:
def setup(self):
self.driver = init_driver()
self.page = Page(self.driver)
def teardown(self):
time.sleep(5)
self.driver.quit()
@pytest.mark.parametrize(("name", "phone"), [("zhangsan", "18888888888"), ("lisi",
"13333333333"), ("wangwu", "17777777777")])
def test_contact(self, name, phone):
# 主页 - 点击 新建联系人
self.page.contact_list.click_add_contact()
# 新建联系人 - 输入 姓名
self.page.new_contact.input_name(name)
# 新建联系人 - 输入 电话
self.page.new_contact.input_phone(phone)
3.3 数据驱动应用
3.3.1 需求
将 scripts/test_contact.py 中的 @pytest.mark.parametrize((“name”, “phone”), [(“zhangsan”,“18888888888”), (“lisi”,“13333333333”), (“wangwu”, “17777777777”)]) 的数据部分改为 yaml 形式编写
3.3.2 yaml 数据和脚本之间的对应关系
分析
我们在实际工作中,不同的项目可能会遇到很多有变化的情况,以下来列举这些 “变数”
- 一个项目有多个 “模块” 都需要使用数据
- 一个模块有多个 “测试脚本” 都需要使用数据
- 一个测试脚本有多个 “用例” 都需要使用数据
其实,一个项目无非就是这些 “变数” ,只要大家掌握yaml数据和这些“变数”的对应关系,那么就不难了。
举例:
接下来我们来用测试用例和 yaml 数据最终的结果进行一个对比。
yaml数据如下:
login_data.yaml
test_login:
test_login_001:
username: "zhangsan"
password: "zhangsan123"
test_login_002:
username: "lisi"
password: "lisi123"
test_login_003:
username: "wangwu"
password: "wangwu123"
sign_up_data.yaml
test_username_sign_up:
test_username_sign_up_001:
username: "zhangsan"
password: "zhangsan321"
test_username_sign_up_002:
username: "lisi"
password: "lisi321"
test_phone_sign_up:
test_phone_sign_up_001:
phone: "13333333333"
password: "123000"
test_phone_sign_up_002:
phone: "18888888888"
password: "321000"
结论
一个 yaml 数据文件 对应 一个模块
- sign_up_data.yaml 对应 注册
- login_data.yaml 对应 登录
数据内容中最外层 key 对应 一个模块下的脚本名
- test_username_sign_up 对应 函数test_username_sign_up
- test_phone_sign_up 对应 函数 test_phone_sign_up
数据内容中第二层 key 对应 用例编号 - test_username_sign_up_001 对应 编号 test_username_sign_up_001
- test_username_sign_up_002 对应 编号 test_username_sign_up_002
数据内容中最里层 key 对应 用例的具体数据 - username 对应 用户名
- password 对应 密码
通讯里中的数据
contact_data.yaml
test_contact:
test_contact_001:
name: "zhangsan"
phone: "18888888888"
test_contact_002:
name: "lisi"
phone: "13333333333"
test_contact_003:
name: "wangwu"
phone: "17777777777"
3.3.3 yaml 数据的解析
分析
装饰器 @pytest.mark.parametrize 中,接收两个参数,第一个为参数名,第二个为列表。我们需要单独写一套解析方法即可,为了后期也可以解析其他的文件,我们直接将解析内容写到 base 中即可。
代码
base/base_analyze.py
import os
import yaml
def analyze_file(file_name, key):
with open(".%sdata%s%s" % (os.sep, os.sep, file_name), "r") as f:
case_data = yaml.load(f)[key]
data_list = list()
for i in case_data.values():
data_list.append(i)
return data_list
3.3.4 调整测试脚本
scripts/test_contact.py
import time
import pytest
from base.base_analyze import analyze_file
from base.base_driver import init_driver
from page.page import Page
class TestContact:
def setup(self):
self.driver = init_driver()
self.page = Page(self.driver)
def teardown(self):
time.sleep(5)
self.driver.quit()
@pytest.mark.parametrize("args", analyze_file("contact_data.yaml", "test_contact"))
def test_contact(self, args):
name = args["name"]
phone = args["phone"]
# 主页 - 点击 新建联系人
self.page.contact_list.click_add_contact()
# 新建联系人 - 输入 姓名
self.page.new_contact.input_name(name)
# 新建联系人 - 输入 电话
self.page.new_contact.input_phone(phone)
3.3.5 增加断言
脚本并不是执行完了之后就算是通过。而是执行完后效果也正确,才能算通过。我们应该再最后增加一个断言的判断。
思路
输入完姓名和电话后,点击返回。会展示保存的联系人页面,如果这个页面上展示的是我们刚刚存的联系人姓名,就可以认为是成功的。
步骤
- 在添加联系人的页面点击返回
在base中封装一个发送按键到手机的方法 - 创建保存的联系人页面对应的文件
- 在这个文件中写一个获取标题的方法
resource-id 为 “com.android.contacts:id/large_title” - 断言传入的名字和获取的名字是否相同
核心代码
page/saved_contact_page.py
from selenium.webdriver.common.by import By
from base.base_action import BaseAction
class SavedContactPage(BaseAction):
# 姓名输入框
name_title_feature = By.ID, "com.android.contacts:id/large_title"
def get_name_title_text(self):
return self.find_element(self.name_title_feature).text
page/page.py
from page.contact_list_page import ContactListPage
from page.new_contact_page import NewContactPage
from page.saved_contact_page import SavedContactPage
class Page:
def __init__(self, driver):
self.driver = driver
@property
def contact_list(self):
return ContactListPage(self.driver)
@propert
def new_contact(self):
return NewContactPage(self.driver)
@property
def saved_contact_page(self):
return SavedContactPage(self.driver)
scripts/test_contact.py
import time
import pytest
from base.base_analyze import analyze_file
from base.base_driver import init_driver
from page.page import Page
class TestContact:
def setup(self):
self.driver = init_driver()
self.page = Page(self.driver)
def teardown(self):
time.sleep(5)
self.driver.quit()
@pytest.mark.parametrize("args", analyze_file("contact_data.yaml", "test_contact"))
def test_contact(self, args):
name = args["name"]
phone = args["phone"]
# 主页 - 点击 新建联系人
self.page.contact_list.click_add_contact()
# 新建联系人 - 输入 姓名
self.page.new_contact.input_name(name)
# 新建联系人 - 输入 电话
self.page.new_contact.input_phone(phone)
# 断言:name 是否和 获取的name一致
assert name == self.page.saved_contact_page.get_name_title_text()