介绍
这次我们要测试的app其实严格来说是一个混合型的APP,因为它里面嵌套了webview。但这里我们先不去测webview的场景,而是测全原生的测试场景,所以先把它当做是一款原生型的app。
对于原生型app,定位时可以全程使用原生app开发者工具:uiautomator2级appium inspector进行定位。
框架设计
在框架设计上,app自动化测试与web自动化测试并无差别,仍然可以沿用web自动化测试的框架,其根本原因在于appium本来就是继承自selenium的。
PO设计
在PO设计上,app自动化测试与web测试相比就有些差别了。
我们知道,web自动化测试中的PO是以域名(不包括后面的参数)来划分页面的。比如说登录页是一个页面,登录成功后跳转到个人中心页是另一个页面。但app自动化测试就不一样了,它是根据功能来划分页面的。
举个例子,像上面的三个页面,第一个是未登录的个人中心页,第二个是登录页,第三个是已登录的个人中心页,它们的功能都是一致的,都是用户管理的相关功能,且都在我的柠檬这一栏中。所以这三个看似不同的页面实则可以归到同一个PO中。
代码实战
通用页
在app中,我们可以发现下方有三个菜单,分别是:首页、题库、我的柠檬,所有的页面都是以这三个菜单为起点展开的,进入任一个页面,都需要点击这三个菜单中的其中一个。所以,这里我们可以把这三个菜单的切换封装到一个PO中。
- navpage.py
from common.basepage import BasePage
from middleware.page.userpage import Userpage
from middleware.page.homepage import Homepage
from middleware.page.exampage import Exampage
from appium.webdriver.common.mobileby import MobileBy
class Navpage(BasePage):
#元素定位
home_locator = (MobileBy.ID,"com.lemon.lemonban:id/navigation_home")
examination_locator = (MobileBy.ID,"com.lemon.lemonban:id/navigation_tiku")
my_lemon_locator = (MobileBy.ID, "com.lemon.lemonban:id/navigation_my")
#点击'主页'按钮
def click_home_btn(self):
self.click(self.home_locator)
return Homepage(self.driver)
#点击'题库'按钮
def click_examation_btn(self):
self.click(self.examination_locator)
return Exampage(self.driver)
#点击'我的柠檬'按钮
def click_mylemon_btn(self):
self.click(self.my_lemon_locator)
return Userpage(self.driver)
登录失败用例
场景分析
如下,首先进入app后,我们是在首页页面的。接着切换至‘我的柠檬’页:
在‘我的柠檬’页之后,点击‘点击头像登录’按钮:
接着跳转至登录页,输入空的用户名、密码,点击登录,弹出toast,验证toast中的文案是否符合预期:
测试步骤
1.在首页中,点击’我的柠檬‘按钮
2.在我的柠檬页中,点击‘点击头像登录’按钮
3.在’登录页’,输入用户名、密码,点击登录按钮
4.定位toast弹框中的字符
前置条件
在登录失败的场景中,业务上来说是没有前置条件的,但代码层面来说,前置条件就是初始化driver。
- conftest.py
import pytest
from appium import webdriver
# 定义测试夹具
@pytest.fixture()
def driver():
"""前置条件"""
#准备desired_capabilities
caps = {
"platformName": "Android",
"deviceName": "emulator-5554",
"appPackage": "com.lemon.lemonban",
"appActivity": ".activity.MainActivity",
"automationName": "UiAutomator2",
}
#初始化客户端
driver = webdriver.Remote(
command_executor='http://127.0.0.1:4723/wd/hub',
desired_capabilities=caps,
)
#隐性等待
driver.implicitly_wait(10)
yield driver
"""后置条件"""
driver.quit()
代码
- userpage.py
from appium.webdriver.common.mobileby import MobileBy
from common.basepage import BasePage
class Userpage(BasePage):
#元素定位
head_to_login_btn_locator = (MobileBy.ID,"com.lemon.lemonban:id/fragment_my_lemon_avatar_layout")
username_locator = (MobileBy.ID,"com.lemon.lemonban:id/et_mobile")
password_locator = (MobileBy.ID,"com.lemon.lemonban:id/et_password")
login_btn_locator = (MobileBy.ID,"com.lemon.lemonban:id/btn_login")
toast_locator = (MobileBy.XPATH,"//android.widget.Toast")
#点击'点击头像登录'按钮
def click_head_to_login_btn(self):
self.click(self.head_to_login_btn_locator)
return self
#点击'登录'按钮
def click_login_btn(self,username,password):
self.write(self.username_locator,username)
self.write(self.password_locator,password)
self.click(self.login_btn_locator)
return self
#获取登录失败的toast文案
def get_login_error_meg(self):
toast = self.wait_element_presence(self.toast_locator)
return toast.text
- test_login.py
import conftest
from middleware.page.navpage import Navpage
from data import login_data
import pytest
login_success_data = login_data.login_success
login_failed_data = login_data.login_failed
class TestLogin():
#登录成功用例
@pytest.mark.parametrize("test_data",login_success_data)
def test_success(self,driver,test_data):
"""
测试步骤:
1.在首页中,点击‘我的柠檬’按钮
2.在‘我的柠檬’页,点击‘点击头像登录’按钮
3.输入用户名、密码,点击登录按钮
"""
actual_result = Navpage(driver).\
click_mylemon_btn().\
click_login_btn(test_data["username"],test_data["password"])
#登录失败用例
@pytest.mark.parametrize("test_data", login_failed_data)
def test_failed(self,test_data,driver):
"""
测试步骤:
1.在首页中,点击’我的柠檬‘按钮
2.在我的柠檬页中,点击‘点击头像登录’按钮
3.在'登录页',输入用户名、密码,点击登录按钮
4.定位toast弹框中的字符
"""
actual_result = Navpage(driver).\
click_mylemon_btn().\
click_head_to_login_btn().\
click_login_btn(username=test_data["username"],password=test_data["password"]).\
get_login_error_meg()
assert actual_result == test_data["expected"]
进入题库用例
场景分析
进入题库,首先得先登录:
接着点击题库,选择“逻辑思维题”
进入题库详情页后,验证title是否符合预期:
测试步骤
1.登录
2.首页中,点击‘题库’按钮
3.进入题库列表页,点击其中一个题库
4.进入题库详情页,验证title
前置条件
进入题库前,有一个前置条件,就是登录。这里登录的相关操作应该写到conftest.py中,这是pytest中专门负责测试夹具的一个模块。
在登录函数中,我们直接调用userpage中的登录相关方法进行登录,最终要返回一个driver对象,因为在要调用登录前置条件的测试用例中,调用PO需要传入driver对象。
#登录
@pytest.fixture()
def login(driver):
user_data = Handler.yaml["user"]
name = user_data["mobile_phone"]
pwd = user_data["pwd"]
Navpage(driver).\
click_mylemon_btn().\
click_head_to_login_btn().\
click_login_btn(name,pwd)
yield driver
问题:要找的元素在页面下方需要滑动怎么办?
在选择逻辑思维题库的时候,遇到了个问题。
一开始进入题库的时候,是无法找到逻辑思维题库的,需要滑动至下方才能找到:
这里就有个问题了,怎么通过滑动屏幕来寻找元素?
可以想到的是,我们可以通过driver的swipe方法进行滑动,然后再定位元素。接下来又有两个问题:
- 往那边划?
- 划多少次才能找到元素?
对于第一个问题很好回答,元素是在下方,所以应该往上滑动屏幕。
对于第二个问题,如果我滑动一次就能找到元素,那当然是最理想的。但真实情况往往是页面很长,需要滑动多次才能找到元素。那么该如何设置滑动的次数,以及找到元素后停止滑动呢?可以写一个循环,循环中写上滑动及寻找元素的相关代码,当满足某个条件后,停止滑动。那么这里的终止滑动的条件应该是什么呢?这里提供两个方案:一个是按时间来滑动,当达到某个时间就退出循环;另一个是按次数来滑动,达到某个次数就退出循环,这两个方案都是可以的。这里说下按时间来滑动的方案。
按时间来滑动的话,我们可以设置一个超时时间,达到超时时间后就退出循环,不再滑动,然后设置滑动一次所需要的时间,每次滑动后都加上这个时间。当总的滑动时间小于超时时间时,则继续滑动;大于超时时间时,则找不到元素,抛出Timeout异常:
#选择其中一个题库
def click_exam(self,name,timeout=10,poll=0.2):
"""
设置一个循环,如果在超时时间内没有找到元素,则会一直上滑并寻找含有该题库名的元素;
如果超过了超时时间,则抛Timeout异常
"""
used_time = 0
while used_time < timeout:
try:
#这里重新设置隐性等待是因为如果该元素如果没找到,则需要隐性等待10秒,这10秒是没必要等的,所以重新设一个较短的等待时间
self.driver.implicitly_wait(0.2)
element = self.find_element((MobileBy.XPATH,f"//*[contains(@text, '{name}')]"))
#在找到了元素之后,再把隐性等待重置回原来的值
self.driver.implicitly_wait(10)
element.click()
#由于点击题库后跳转的下个页面还是在本页面中,所以返回self
return self
#如果find_element()没找到,就会抛出timeout异常,所以这里也要捕获timeout异常,当没找到元素时,执行except下的代码
except TimeoutError as e:
#没有找到元素就进行滑屏,因为说不定要定位的元素就在屏幕下方
self.swipe_up()
#滑动之后等待一下
time.sleep(poll)
used_time += poll
#如果超过超时时间还是找不到元素,则抛出Timeout,异常
raise TimeoutError("找不到题库")
代码
- exampage.py
from common.basepage import BasePage
from appium.webdriver.common.mobileby import MobileBy
import time
class Exampage(BasePage):
#定位元素
title_name_locator = (MobileBy.ID,"com.lemon.lemonban:id/category_title")
#选择其中一个题库
def click_exam(self,name,timeout=10,poll=0.2):
"""
设置一个循环,如果在超时时间内没有找到元素,则会一直上滑并寻找含有该题库名的元素;
如果超过了超时时间,则抛Timeout异常
"""
used_time = 0
while used_time < timeout:
try:
#这里重新设置隐性等待是因为如果该元素如果没找到,则需要隐性等待10秒,这10秒是没必要等的,所以重新设一个较短的等待时间
self.driver.implicitly_wait(0.2)
element = self.find_element((MobileBy.XPATH,f"//*[contains(@text, '{name}')]"))
#在找到了元素之后,再把隐性等待重置回原来的值
self.driver.implicitly_wait(10)
element.click()
#由于点击题库后跳转的下个页面还是在本页面中,所以返回self
return self
#如果find_element()没找到,就会抛出timeout异常,所以这里也要捕获timeout异常,当没找到元素时,执行except下的代码
except TimeoutError as e:
#没有找到元素就进行滑屏,因为说不定要定位的元素就在屏幕下方
self.swipe_up()
#滑动之后等待一下
time.sleep(poll)
used_time += poll
#如果超过超时时间还是找不到元素,则抛出Timeout,异常
raise TimeoutError("找不到题库")
#获取title
def get_title(self):
return self.find_element(self.title_name_locator).text
- test_exam.py
import conftest
from middleware.page.navpage import Navpage
from data.exam_data import logic_exam
import pytest
class TestExam():
@pytest.mark.parametrize("test_data",logic_exam)
def test_exam(self,login,test_data):
"""
测试步骤:
1.登录
2.首页中,点击‘题库’按钮
3.进入题库列表页,点击其中一个题库
4.进入题库详情页,验证title
"""
driver = login
actual_result = Navpage(driver).\
click_examation_btn().\
click_exam(test_data["exam_type"]).\
get_title()
assert actual_result == test_data["except"]