Appium2 总结(四)appium-pytest-aullure-PO 模型自动化框架

0. 先展示成果

1. 什么是自动化测试 PO 模型

自动化测试 PO 模型,即页面对象 (Page Object) 模型,是一种在自动化测试中常用的设计模式。它主要用于提高测试代码的可维护性、可读性和复用性。PO 模型的核心思想是将页面元素的定位器和操作封装到一个类中,这个类代表了页面的界面。

2.PO 模型的主要特点:

(1)封装性:页面元素的定位器(如 ID、类名、XPath 等)和与元素相关的操作都被封装在页面对象类中。

(2)可复用性:页面对象类可以在多个测试场景中复用,减少了代码的重复。

(3)低耦合:测试脚本与页面元素的定位器解耦,当页面元素发生变化时,只需修改页面对象类,而不需要修改测试脚本。

(4)易于维护:当页面元素更新或移动时,只需在页面对象类中更新定位器,测试脚本不需要修改。

3.PO 模型的实现步骤:

(1)创建页面类:为每个页面创建一个页面类,类名通常与页面功能相关。

(2)定义元素定位器:在页面类中定义所有需要操作的元素的定位器。

(3)实现元素操作:为页面类提供方法来实现对页面元素的操作,如点击、输入文本等。

(4)使用页面对象:在测试脚本中,通过页面对象来访问和操作页面元素。

4.🌰

(1)目录结构如下图:

image-20240810023833669

(2)代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# base/base_page.py
from appium import webdriver
from appium.webdriver.common.appiumby import AppiumBy

from conftest import android_driver


class BasePage:

def __init__(self, driver: webdriver = None):
self.driver = driver

# id 定位
def by_id(self, id_value: str):
return self.driver.find_element(AppiumBy.ID, id_value)

# xpath 定位
def by_xpath(self, xpath_value: str):
return self.driver.find_element(AppiumBy.XPATH, xpath_value)

# accessibility_id定位
def by_accessibility_id(self, accessibility_id_value: str):
return self.driver.find_element(AppiumBy.ACCESSIBILITY_ID, accessibility_id_value)

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
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
# common/get_log.py
import os
import logging


class GetLog:
def __init__(self, name, level=logging.DEBUG):
# 创建一个 log 对象
# 其中的__name__代表当前 py 文件的名称,也可以不写
self.logger = logging.getLogger(name)
self.logger.handlers.clear() # 防止重复输出日志,因为添加一个 handler就会输出一次
# 设置 log 对象的等级
self.logger.setLevel(level)

log_path = os.path.join(os.path.dirname(__file__), name + '.log')

# 创建一个 handler,将写入文件
fh = logging.FileHandler(log_path, mode='a')
# 设置log 等级与前面保持一致
fh.setLevel(level)

# 创建一个 handler, 将 log 输出到控制台
ch = logging.StreamHandler()
# 设置 log 等级与 log 对象保持一致
ch.setLevel(level)

# 设置输出格式
log_formatter = '%(asctime)s - %(filename)s[line:%(lineno)d] - %(levelname)s : %(message)s'
formatter = logging.Formatter(log_formatter)
ch.setFormatter(formatter)
fh.setFormatter(formatter)

# 把handler添加到 logger 里
# print(logger.handlers) # 没添加时,logger.handlers == []
# 如果 logger.handlers 为空,则添加 ch / fh ,防止重复输出日志,因为添加一个 handler就会输出一次
# if logger.handlers.__len__() == 0:
self.logger.addHandler(ch)
self.logger.addHandler(fh)

# print(logger.handlers)

def debug(self, msg):
"""
记录debug级别的日志
:param msg: 日志消息
"""
self.logger.debug(msg)

def info(self, msg):
"""
记录info级别的日志
:param msg: 日志消息
"""
self.logger.info(msg)

def warning(self, msg):
"""
记录warning级别的日志
:param msg: 日志消息
"""
self.logger.warning(msg)

def error(self, msg):
"""
记录error级别的日志
:param msg: 日志消息
"""
self.logger.error(msg)

def critical(self, msg):
"""
记录critical级别的日志
:param msg: 日志消息
"""
self.logger.critical(msg)

# 使用示例


if __name__ == '__main__':
log = GetLog(__name__)
log.debug('This is a debug message')
log.info('This is an info message')
log.warning('This is a warning message')
log.error('This is an error message')
log.critical('This is a critical message')

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
# page/login_page.py
from base.base_page import BasePage


class LoginPage(BasePage):

# 勾选协议
def agree_protocol(self):
self.by_id('com.xintiaotime.yoy:id/agree_protocol_checkbox').click()

# 点击本机号码登录按钮
def local_phone_btn(self):
self.by_id('com.xintiaotime.yoy:id/local_phone_one_click_login_btn').click()

# 选择其他手机号码登录
def other_phone_login_btn(self):
self.by_id('com.xintiaotime.yoy:id/other_phone_login_btn').click()

# 输入电话号码
def input_phone_number(self, phone_number):
self.by_id('com.xintiaotime.yoy:id/input_phone_number_editText').send_keys(phone_number)

# 点击获取验证码
def get_phone_verification(self):
self.by_id('com.xintiaotime.yoy:id/get_phone_verification_code_btn').click()

# 填写验证码
def input_phone_verification(self, verification_code):
self.by_id('input_phone_verification_code_editText').send_keys(verification_code)

# 获取 toast 中“验证码错误,请重试”的文本
def get_verification_code_error_toast(self):
return self.by_xpath('//android.widget.Toast[@text="验证码错误,请重试"]').text

# 显示元素“请先阅读并同意协议”
def show_read_and_agreed_btn(self):
return self.by_id('com.xintiaotime.yoy:id/not_agreed_protocol_hint').is_displayed()

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
# testcases/test_login_page.py
import allure

from page.login_page import LoginPage


@allure.epic('测试登录')
class TestLogin:

@allure.story('测试直接点击其他手机号码登录')
def test_login_01(self, android_driver):
login_page = LoginPage(android_driver)
with allure.step('点击其他手机号码登录按钮'):
login_page.local_phone_btn()
with allure.step('显示元素“请先阅读并同意协议”'):
assert login_page.show_read_and_agreed_btn() is True

@allure.story('点击同意协议复选框2次')
def test_login_02(self, android_driver):
login_page = LoginPage(android_driver)
with allure.step('点击同意协议复选框2次,让“请先阅读并同意协议”消失'):
login_page.local_phone_btn()
login_page.local_phone_btn()

@allure.story('测试直接点击其他手机号码登录')
def test_login_03(self, android_driver):
login_page = LoginPage(android_driver)
with allure.step('点击其他手机号码登录按钮'):
login_page.other_phone_login_btn()
with allure.step('显示元素“请先阅读并同意协议”'):
assert login_page.show_read_and_agreed_btn() is True

@allure.story('测试其他电话登录')
def test_login_04(self, android_driver):
login_page = LoginPage(android_driver)
with allure.step('点击同意协议复选框'):
login_page.agree_protocol()
with allure.step('点击其他手机号码登录按钮'):
login_page.other_phone_login_btn()
with allure.step('输入电话号码'):
login_page.input_phone_number('17312341234')
with allure.step('点击获取短信验证码'):
login_page.get_phone_verification()
with allure.step('输入验证码'):
login_page.input_phone_verification('1234')

assert login_page.get_verification_code_error_toast() == '验证码错误,请重试'

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
69
70
71
72
73
74
# conftest.py
import pytest

from appium import webdriver
from appium.options.android import UiAutomator2Options
from appium.options.ios import XCUITestOptions
from appium.webdriver.appium_service import AppiumService

APPIUM_PORT = 4723
APPIUM_HOST = "127.0.0.1"

# 华为真机配置
capabilities = dict(
platformName='Android',
platformVersion='12',
deviceName='TAS-AN00',
automationName='uiautomator2',
skipServerInstallation=False,
appPackage='com.xintiaotime.yoy', # 包名
appActivity='.ui.guide.SplashActivity', # 界面名,这个界面名字一定要用 monkey 找,要补就要报错。。。
# 不清空缓存信息,保存登录信息
noReset=True,
)


@pytest.fixture(scope="session")
def appium_service():
pass


def create_ios_driver(custom_opts=None):
pass


def create_android_driver(custom_opts=capabilities):
"""Create Android driver."""
options = UiAutomator2Options()
if custom_opts is not None:
options.load_capabilities(custom_opts)
# Appium1 points to http://127.0.0.1:4723/wd/hub by default
driver = webdriver.Remote(f"http://{APPIUM_HOST}:{APPIUM_PORT}", options=options)
driver.implicitly_wait(15)
return driver


@pytest.fixture
def ios_driver_factory():
"""ios_driver_factory"""
return create_ios_driver


@pytest.fixture
def ios_driver():
"""return ios_driver to the function using it, quit driver afterwards."""
# prefer this fixture if there is no need to customize driver options in tests
driver = create_ios_driver()
yield driver
driver.quit()


@pytest.fixture
def android_driver_factory():
"""android_driver_factory"""
return create_android_driver


@pytest.fixture
def android_driver():
"""return android_driver to the function using it, quit driver afterwards."""
# prefer this fixture if there is no need to customize driver options in tests
driver = create_android_driver()
yield driver
driver.quit()

1
2
3
4
5
6
7
8
9
10
11
12
13
# run_test.py
import os

import pytest

if __name__ == '__main__':
# pytest.main([])
# 带参数执行
# --clean-alluredir每次情况生成的json文件地址
pytest.main(['-v', '-s', '--clean-alluredir', '--alluredir', './allure-results'])
# allure generat + 数据源文件目录 -o + 报告的目录
os.system('allure generate ./allure-results -o ./report --clean')

(3)allure 生成报告截图

image-20240810024915399

image-20240810024957661