一 、 PO 是什么
-
页面对象模型(PO)是一种设计模式,全称(Page Object),用来管理维护一组web元素的对象库
-
在PO下,应用程序的每一个页面都有一个对应的page class
- 每一个page class维护着该web页的元素集和操作这些元素的方法
- page class中的方法命名最好根据对应的业务场景进行,例如通常登录后我们需要等待几秒钟,
- 多用于UI自动化测试(例: Web ,APP等)
二、代码结构
1. PageObject层页面对象内容同包含:
1) 对象属性(全局变量) = = 页面的元素
python中,页面元素定位表达式表示,建议如下格式:
ele_loactor = (By.XPATH, "XPath表达式")
当然:定位方式也可以是其他,比如By.ID,By.Name等
java中,页面元素定位表达式,建议如下格式:
public static final By ele_loc = By.xpath("元素定位表达式")
2) 对象的行为 == 页面的元素的操作(如:页面点击、输入等等)
三 、 代码实战
这里以 http://120.78.128.25:8765/Index/login.html 登录功能测试为例
1. python实现:
1) LoginPage封装:
from selenium.webdriver.common.by import By
from selenium.webdriver.support.wait import WebDriverWait
from selenium.webdriver.remote.webdriver import WebDriver
from selenium.webdriver.support import expected_conditions as EC
class LoginPage:
current_url = "http://120.78.128.25:8765/Index/login.html"
# 登录框
box_loc = (By.XPATH, '//form[@name="login-form"]')
# 账号输入框
user_loc = (By.XPATH, '//input[@name="phone"]')
# 密码输入框
pwd_loc = (By.XPATH, '//input[@name="password"]')
# 登录按钮
but_login_loc = (By.XPATH, "//button[contains(text(),'登录')]")
# 表单错误提示
error_loc = (By.XPATH, '//div[@class="form-error-info"]')
# 页面中间错误提示
center_error_loc = (By.XPATH, '//div[@class="layui-layer-content"]')
def __init__(self, driver: WebDriver):
self.driver = driver
def check_login_box_exist(self):
try:
# 先等待登录框可见
WebDriverWait(self.driver, 20).until(EC.visibility_of_element_located(self.box_loc))
except Exception as e:
return False
else:
return True
def login(self, user, pwd):
""" 登录
:param user: 用户账号
:param pwd: 用户密码
:return: None
"""
# 先等待登录框可见
if self.check_login_box_exist():
# 定位账号输入框,并输入账号
ele = self.driver.find_element(*self.user_loc)
ele.clear() # 清除输入框内容
ele.send_keys(user) # 输入框输入指定文案
# 定位密码输入框,并输入账号
ele = self.driver.find_element(*self.pwd_loc)
ele.clear()
ele.send_keys(pwd)
# 定位登录按钮并点击
self.driver.find_element(*self.but_login_loc).click()
def get_form_error_info(self):
""" 获取错误提示内容:账号位数不对、密码未输入 """
WebDriverWait(self.driver, 20).until(EC.visibility_of_element_located(self.error_loc))
return self.driver.find_element(*self.error_loc).text
def get_center_error_info(self):
""" 获取页面中间错误提示内容:密码错误、账号未授权 """
WebDriverWait(self.driver, 20).until(EC.visibility_of_element_located(self.center_error_loc))
error_text = self.driver.find_element(*self.center_error_loc).text
return error_text
2. LoginTest测试:
@ddt
class TestLogin(unittest.TestCase):
@classmethod
def setUpClass(cls):
cls.driver = webdriver.Chrome()
cls.driver.maximize_window()
cls.driver.get("http://120.78.128.25:8765/Index/login.html")
@classmethod
def tearDownClass(cls):
# 会话关闭,关闭浏览器
cls.driver.close()
cls.driver.quit()
# 用例初始化以及清除操作,每次打开浏览器,关闭浏览器会比较耗时,如果所有用例的初始化(即:预置条件)是一样的,那么可以将操作放置类初始化和清除函数中
# def setUp(self):
# self.driver = webdriver.Chrome()
# self.driver.get(LoginPage(self.driver).current_url)
# self.driver.maximize_window()
#
# def tearDown(self):
# self.driver.quit()
# 输入不合法数据
failture_data_input_invaild = [
{'title': '密码为空', 'TestDatas': ['18684720553', ''], 'expect': '请输入密码'},
{'title': '号码位数不足11位', 'TestDatas': ['1868472055', '123456'], 'expect': '请输入正确的手机号'},
{'title': '号码位数大于11位', 'TestDatas': ['186847205537', '123456'], 'expect': '请输入正确的手机号'}]
# 密码错误、号码未授权
failture_data_number_not_allowed = [
{'title': '帐号或密码错误!', 'TestDatas': ['18684720553', 'python123'], 'expect': '帐号或密码错误!'},
{'title': '号码未授权', 'TestDatas': ['15934810000', '123456'], 'expect': '此账号没有经过授权,请联系管理员!'}]
def test_login_success(self):
""" 登录成功后,检测界面跳转后,”我的账户“元素是否存在"""
# 调用OP中Login封装类中登录方法
LoginPage(self.driver).login(RightPhone,RightPwd)
# 断言
self.assertTrue(IndexPage(self.driver).check_account_sign_exist())
@data(*failture_data_input_invaild)
def test_login_failure_for_invaild_input(self, one_invaild):
""" 登录失败:无密码、账号位数不对"""
# 登录类实例对象
lp = LoginPage(self.driver)
# 调用OP中Login封装类中登录方法
lp.login(*one_invaild['TestDatas'])
# 错误提示预期:
error_expect = one_invaild['expect']
# 断言
self.assertEqual(lp.get_form_error_info(), error_expect)
@data(*failture_data_number_not_allowed)
def test_login_center_error(self, one_invaild):
""" 登录失败:账号未授权、密码错误"""
# 登录类实例对象
lp = LoginPage(self.driver)
# 调用OP中Login封装类中登录方法
lp.login(*one_invaild['TestDatas'])
# 错误提示预期:
error_expect = one_invaild['expect']
# 断言
self.assertEqual(lp.get_center_error_info(), error_expect)
2 、 Java实现
1) LoginPage封装:
package com.WebUI.Pages;
import org.openqa.selenium.By;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.support.ui.WebDriverWait;
import org.openqa.selenium.support.ui.ExpectedConditions;
import com.WebUI.Tests.TestLogin;
public class LoginPage {
/**
* 页面元素属性
*/
private By account_input_loc=By.xpath("//input[@name='phone']");
private By password_input_loc=By.xpath("//input[@name='password']");
private By remember_phone_loc = By.xpath("//input[@type='checkbox']");
private By forgot_pwd_loc = By.xpath("//a[contains(text(),'忘记密码')]");
private By login_button_loc = By.xpath("//button[text()='登录']");
private By free_register_loc = By.xpath("//a[contains(text(),'免费注册')]");
private By pwdOrAccountNull_error_loc = By.xpath("//div[@class='form-error-info']");
private By verify_img_loc =By.xpath("//div[@name='notify_url']") ;
// private By verify_input_loc =
// 密码错误或者账号无权限或者账号未注册时,错误提示
private By accountNoExit_error_loc = By.xpath("//div[@class='layui-layer-content']");
/**
* 判断当前页面是否是登录界面
* @return 是否是登录页面
*/
public boolean currentPageisLoginIdle() {
String title = TestLogin.driver.getTitle();
if (title.equals("前程贷官网 - 欢迎登录")) {
return true;
}
return false;
}
/**
* 判断元素是否存在
* @param locator : 元素定位表达式
* @return 元素存在与否状态
*/
public boolean eleIsExist(final By locator ) {
WebDriverWait wait =new WebDriverWait(TestLogin.driver,20);
try {
wait.until(ExpectedConditions.presenceOfElementLocated(locator));
return true;
}catch(Exception e) {
return false;
}
}
/**
* 判断元素是否可见
* @param locator : 元素定位表达式
* @return 元素可见与否状态
*/
public boolean eleIsVisible(final By locator ) {
WebDriverWait wait =new WebDriverWait(TestLogin.driver,20);
try {
wait.until(ExpectedConditions.visibilityOfElementLocated(locator));
return true;
}catch(Exception e) {
return false;
}
}
/**
* 指定input元素输入内容
* @param locator : 元素定位表达式 By
* @param content : 输入内容
*/
public void inputContent(final By locator,String content) {
if (eleIsVisible(locator)) {
WebElement ele = TestLogin.driver.findElement(locator);
ele.clear();
ele.sendKeys(content);
}
}
/**
* 仅输入账号
* @param phone :账号/号码
*/
public void inputAccountInfo(String phone) {
inputContent(account_input_loc,phone);
}
/**
* 输入账号密码
* @param phone : 账号
* @param pwd: 密码
*/
public void inputAccountInfo(String phone,String pwd) {
inputContent(account_input_loc,phone);
inputContent(password_input_loc,pwd);;
}
/**
* 点击登录按钮
*/
public void clickLoginButton() {
if (eleIsVisible(login_button_loc)) {
TestLogin.driver.findElement(login_button_loc).click(); }
}
public String getFormErrorInfo() {
if (eleIsVisible(pwdOrAccountNull_error_loc)) {
return TestLogin.driver.findElement(pwdOrAccountNull_error_loc).getText();
}
return "";
}
public String getCenterError_info() {
// 获取页面中间错误提示内容:密码错误、账号未授权 """
if (eleIsVisible(accountNoExit_error_loc)) {
return TestLogin.driver.findElement(accountNoExit_error_loc).getText();
}
return "";
}
}
2. 测试类:
package com.WebUI.Tests;
import com.WebUI.Utils.OpenBrowser;
import com.WebUI.Pages.LoginPage;
import com.WebUI.Utils.Constants;
import java.util.concurrent.TimeUnit;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebDriver.Navigation;
import org.testng.Assert;
import org.testng.annotations.AfterTest;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.BeforeTest;
import org.testng.annotations.Parameters;
import org.testng.annotations.Test;
public class TestLogin {
public static WebDriver driver;
LoginPage LP = new LoginPage();
@Parameters({"browserType"})
@BeforeTest
public void setUpTest(String browserType) {
// 浏览器打开,进入登录界面
System.out.println("您所选择浏览器类型为: " + browserType );
driver = OpenBrowser.openBrower(browserType);
driver.get(Constants.LOGIN_URL);
driver.manage().window().maximize();
driver.manage().timeouts().implicitlyWait(10, TimeUnit.SECONDS);
}
@BeforeMethod
public void setUpMethod() {
// 页面刷新
Navigation navigate = driver.navigate();
navigate.refresh();
System.out.println("页面刷新");
}
@AfterTest
public void tearDownTest() {
// 浏览器关闭
// driver.close();
driver.quit();
}
@Test(enabled=false)
public void test_is_login_idle() {
// 地址是否指向登录界面
boolean is_login = LP.currentPageisLoginIdle();
Assert.assertEquals(is_login, true);
}
@Test
public void test_success() {
LP.inputAccountInfo("18684720553");
}
@Test
public void testNoinputLogin() {
// 不输入任何信息直接登录
LP.clickLoginButton();
String errorText = LP.getFormErrorInfo();
Assert.assertEquals(errorText, "请输入手机号");
}
@Test
public void testNoInputPwdLogin() {
// 不输入密码直接登录
LP.inputAccountInfo("18684720553");
LP.clickLoginButton();
String errorText = LP.getFormErrorInfo();
Assert.assertEquals(errorText, "请输入密码");
}
}
结果截图:
四、UI自动化用例设计原则
- 用例之间尽可能不相互干扰(前边用例不影响后边用例执行)
- 每一条用例都可以单独运行
- 用例数据没有环境干扰
单独服务器自动化执行
单独时间去执行(比如,Jekins定时凌晨执行)
五、框架设计 :
-
地址URL常量化
-
浏览器类型,参数化 ----- TestNG xml
- PO 模式 ---- 页面对象(元素/操作) 与 页面测试 分离开
- 页面共性提取 ---- 元素是否存在,元素点击,输入等等操作
- 数据分离 ------ 测试数据可单独管理,测试类中引入使用即可
- 数据驱动 ------ dataProviderClass 和dataProvider
- 业务流程封装:
1) 用例层只需要关注业务;
2) 用例层基于业务组装,更加灵活简单;
3) 实例化业务流程 : 对象数据初始化则需要有参构造以及set/get方法(如:登录实现登录功能,登录函数中包含登录信息输入、点击登录按钮等等)