本文主要记录了笔者从使用 Selenium IDE 录制/回放测试用例开始,过渡到用编写代码的方式实现测试用例(非 Page Object 方式),并用命令行的方式在 Linux 平台执行测试用例,包括在 Linux 平台搭建 Selenium Grid ,然后实现了一个脚本解释器,直接解释执行由 Selenium IDE 录制的三段式测试用例,最后开发了一个测试用例信息管理网站,管理和并发执行测试用例,并生成测试报告发送邮件。
本文共十个阶段,前五个阶段是 Selenium 的基础知识,主要介绍了 Selenium IDE、 Selenium Grid 和 TestNG 的用法,以及如何在 Linux 平台上搭建执行环境和并发运行测试用例, 为后面阶段实现分布式测试做好准备。
后五个阶段主要介绍了如何实现一个测试用例解释执行器,并通过搭建网站的方式管理和运行测试用例,并对这个网站进行了优化。
这是一个网页自动化测试平台,2016 年 1 月份开始编写,断断续续大概 3 个月基本完成,后续也做过一些小的调整和修复。
当时笔者也是刚开始接触学习 Selenium, 想把当时的学习路线、管理平台实现以及遇到的一些问题和解决方法分享给大家,希望能对初学者有一定的帮助和参考。
本文采用的是回忆的写法,因为时间久远,一些开始阶段的详细步骤都已经忘记了,所以写得比较简练,后面阶段会越来越详细。
Java,TestNG,Selenium IDE,WebDriver,Selenium Grid,CentOS,Shell,ThinkPHP,MySQL,Docker(可选)
使用 Selenium IDE 或者 Katalon Recorder 录制/回放测试用例, 录制好的测试用例如下:
open http://www.17zuoye.com
click xpath=(//a[contains(text(),'登录')])[2]
type id=index_login_username 100xxxxxxxx
type id=index_login_password xxxxxx
click id=_a_loginForm
assertLocation http://www.17zuoye.com/teacher/index.vpage
click //a[contains(text(),'布置新作业')]
优点
缺点
使用 Selenium IDE 或者 Katalon Recorder 的导出功能,将测试用例脚本转换成 JAVA + TestNG 代码,然后制作成 jar 包,通过命令行的方式在本机执行。
以下是自动转换后的 Java 代码:
package com.example.tests;
import java.util.regex.Pattern;
import java.util.concurrent.TimeUnit;
import org.testng.annotations.*;
import static org.testng.Assert.*;
import org.openqa.selenium.*;
import org.openqa.selenium.firefox.FirefoxDriver;
import org.openqa.selenium.support.ui.Select;
public class UntitledTestCase {
private WebDriver driver;
private String baseUrl;
private boolean acceptNextAlert = true;
private StringBuffer verificationErrors = new StringBuffer();
@BeforeClass(alwaysRun = true)
public void setUp() throws Exception {
driver = new FirefoxDriver();
baseUrl = "https://www.katalon.com/";
driver.manage().timeouts().implicitlyWait(30, TimeUnit.SECONDS);
}
@Test
public void testUntitledTestCase() throws Exception {
driver.get("http://www.17zuoye.com");
driver.findElement(By.xpath("(//a[contains(text(),'登录')])[2]")).click();
driver.findElement(By.id("index_login_username")).clear();
driver.findElement(By.id("index_login_username")).sendKeys("100xxxxxxxx");
driver.findElement(By.id("index_login_password")).clear();
driver.findElement(By.id("index_login_password")).sendKeys("xxxxxx");
driver.findElement(By.id("_a_loginForm")).click();
assertEquals(driver.getCurrentUrl(), "http://www.17zuoye.com/teacher/index.vpage");
driver.findElement(By.xpath("//a[contains(text(),'布置新作业')]")).click();
}
@AfterClass(alwaysRun = true)
public void tearDown() throws Exception {
driver.quit();
String verificationErrorString = verificationErrors.toString();
if (!"".equals(verificationErrorString)) {
fail(verificationErrorString);
}
}
private boolean isElementPresent(By by) {
try {
driver.findElement(by);
return true;
} catch (NoSuchElementException e) {
return false;
}
}
private boolean isAlertPresent() {
try {
driver.switchTo().alert();
return true;
} catch (NoAlertPresentException e) {
return false;
}
}
private String closeAlertAndGetItsText() {
try {
Alert alert = driver.switchTo().alert();
String alertText = alert.getText();
if (acceptNextAlert) {
alert.accept();
} else {
alert.dismiss();
}
return alertText;
} finally {
acceptNextAlert = true;
}
}
}
优点
缺点
在已经安装 Firefox 或者 Chrome 浏览器的 Linux 服务器上无界面 (虚拟界面) 执行测试用例:
xvfb-run --auto-servernum --server-args="-screen 0 1280x760x24" <执行命令>
xvfb 简介:
xvfb:是通过提供一个类似 X server 守护进程和设置程序运行的环境变量 DISPLAY 来提供程序运行的环境
安装:
yum install -y Xvfb
yum install -y xorg-x11-fonts*
优点
缺点
在 Linux 环境下,搭建 Selenium Grid 集群,可以使用 Docker 的方式搭建,方便快捷。
说明:
Selenium Grid Hub 启动命令:
$ docker run -d -p 4444:4444 --name selenium-hub index.alauda.cn/selenium/hub:2.53.0
Chrome and Firefox Nodes 启动命令:
$ docker run -d --link selenium-hub:hub index.alauda.cn/selenium/node-chrome:2.53.0
$ docker run -d --link selenium-hub:hub index.alauda.cn/selenium/node-firefox:2.53.0
Java 代码里指定使用 Selenium Grid 服务器:
DesiredCapabilities aDesiredcap = DesiredCapabilities.chrome();
aDesiredcap.setPlatform(Platform.ANY)
WebDriver wd = new RemoteWebDriver("http://hub.17zuoye.net:4444/wd/hub", aDesiredcap);
wd.doSomething()
优点
缺点
使用 TestNG 实现并发测试
testng.xml 示例文件:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE suite SYSTEM "http://testng.org/testng-1.0.dtd">
<suite name="Suite" parallel="tests" thread-count="3">
<test name="testrecord_01">
<classes>
<class name="com.zuoye.qa.webtest.TestCase01" />
</classes>
</test>
<test name="testrecord_02">
<classes>
<class name="com.zuoye.qa.webtest.TestCase02" />
</classes>
</test>
<test name="testrecord_03">
<classes>
<class name="com.zuoye.qa.webtest.TestCase03" />
</classes>
</test>
</suite>
执行测试用例命令如下:
$ java -classpath "${SELENIUM_JAR}:${TESTNG_JAR}:${TESTCASE_JAR}" org.testng.TestNG ${TESTNG_XML}
注:
SELENIUM_JAR: selenium-server-standalone 的 jar 包路径
TESTNG_JAR: testng 的 jar 包路径
TESTCASE_JAR: 包含测试用例的 jar 包路径
TESTNG_XML: 自定义的 TestNG 配置文件路径
优点
缺点
用 Java 语言实现一个测试用例解释执行器,逐行解释执行存放在本地文件里的 Selenium IDE 录制的测试用例脚本。
说明:
Selenium IDE 三段式语法组成要素:
command target value
说明:
command:命令,如单击 click
target:目标,即是命令的对象,如单击按钮 (用 xpath 或是其它定位方法表示)
value:值,如向输入框输入东西即在这里设置
注:command 为必须项,target 和 value 可以按需填写
测试脚本样例:
open http://www.17zuoye.com
click xpath=(//a[contains(text(),'登录')])[2]
type id=index_login_username 100xxxxxxxx
type id=index_login_password xxxxxx
click id=_a_loginForm
assertLocation (.*)/teacher/index.vpage(.*)
// 自定义的截图命令
screenShot
click //a[contains(text(),'布置新作业')]
主引擎 - 逐行解释执行器(TestRunner.java 部分截取)
@Test
public void testRunner() throws IOException, InterruptedException {
// 执行结果默认值
String testResult = "passed";
try {
// 从网络获取测试用例内容
BufferedReader bufferedReader = Tools.httpGetRequest(map.get("showTestcaseUrl"));
String lineTxt = null;
// 逐行读取并执行测试命令
while((lineTxt = bufferedReader.readLine()) != null){
// 如果该行是注释语句则忽略
if (lineTxt.startsWith("//")) {
logger.info(lineTxt);
continue;
}
// 如果该行是空白语句则忽略
if (lineTxt.length() <= 0) {
continue;
}
// Java 默认的分隔符是“空格”、“制表符(‘\t’)”、“换行符(‘\n’)”、“回车符(‘\r’)”
// 解析命令行中包含的 command target value
StringTokenizer strTokens = new StringTokenizer(lineTxt);
String command = strTokens.hasMoreTokens() ? strTokens.nextToken() : null;
String target = strTokens.hasMoreTokens() ? strTokens.nextToken() : null;
String value = strTokens.hasMoreTokens() ? strTokens.nextToken() : null;
// 如果 value 值中包含空格,则拼接还原value, command 和 target 中不支持空格
if (strTokens.hasMoreTokens()) {
value = value + " " + strTokens.nextToken();
}
// 执行步数计数器
step++;
// 日志中记录数据解析后的结果
logger.info("step:" + step + " command:[" + command + "] target:[" + target + "] value:[" + value + "]");
// 获取 command 命令对应的类名
String className = CommandTools.getClassByMethod(command);
if (className != null) {
// 每步操作的时间间隔
Thread.sleep(stepSleep);
// screenShot 是截图命令,非 Selenium IDE 原生命令, 另外需要特殊处理,定义图片的名称和生成的位置
if (command.equals("screenShot")) {
if (null != target && target.length() > 0) {
target = map.get("screenshotDir") + "/" + String.format("%03d", step) + "_" + target + ".png";
} else {
target = map.get("screenshotDir") + "/" + String.format("%03d", step) + ".png";
}
}
Class<?> classCommand = Class.forName(className);
Method method = classCommand.getDeclaredMethod(command, WebDriver.class, String.class, String.class);
// 执行命令,并记录执行结果
Object objString = method.invoke(classCommand, driver, target, value);
// 日志中记录运行结果信息,并根据 passed 关键字判断这一条命令是否执行成功
if (objString != null) {
if (objString.toString().startsWith("passed")) {
logger.info("result:[" + objString.toString() + "] ");
} else {
testResult = "failed";
// 记录当前页面链接
logger.severe("CurrentUrl:[" + Tools.getCurrentUrl(driver) + "]");
logger.severe("result:[" + testResult + "] " + objString.toString());
// 运行失败截图
Tools.screenShot(driver, map.get("screenshotDir") + "/" + String.format("%03d", step) + "_failed.png");
}
}
} else {
// 无法识别的 command 命令,command 拼写错误或者本程序没有实现该命令
throw new Exception("无法解析命令行: " + lineTxt);
}
}
bufferedReader.close();
} catch (Exception e) {
testResult = "error";
// 记录当前页面链接
logger.severe("CurrentUrl:[" + Tools.getCurrentUrl(driver) + "]");
logger.severe("运行异常: " + Tools.handleException(e));
e.printStackTrace();
} finally {
// 没有发现可用的命令行
if (step == 0) {
testResult = "error";
logger.severe("没有发现可用的命令行");
} else {
// 执行结束, 截图
Tools.screenShot(driver, map.get("screenshotDir") + "/" + String.format("%03d", step) + "_" + testResult + ".png");
}
if (testResult.equals("passed")) {
logger.info("运行结果:" + testResult);
} else {
logger.severe("运行结果:" + testResult);
}
BufferedReader bufferedReader = Tools.httpGetRequest(map.get("submitTestresultUrl") + testResult);
if (!bufferedReader.readLine().toString().equals("ok")) {
logger.severe("提交运行结果: " + testResult + " 失败.");
}
bufferedReader.close();
}
}
工具类 (Tools.java)
package com.zuoye.qa.webtest;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.lang.reflect.InvocationTargetException;
import java.net.HttpURLConnection;
import java.net.URL;
import org.openqa.selenium.WebDriver;
import com.zuoye.qa.webtest.commands.OtherCommand;
public class Tools {
public Tools() {
// TODO Auto-generated constructor stub
}
// 主引擎中使用的截图命令
public static void screenShot(WebDriver driver, String target) throws IOException, InterruptedException {
OtherCommand.screenShot(driver, target, null);
}
// 请求网络链接,获取测试用例内容
public static BufferedReader httpGetRequest (String Url) throws IOException {
String encoding = "utf-8";
URL url = new URL(Url);
HttpURLConnection httpUrlConn = (HttpURLConnection) url.openConnection();
// 设置是否向httpUrlConnection输出,post请求,参数要放在http正文内,需要设为true, 默认情况下是false, GET请求;
httpUrlConn.setDoOutput(false);
// 设置是否从httpUrlConnection读入,默认情况下是true;
httpUrlConn.setDoInput(true);
// Post 请求不能使用缓存, GET请求也可不用
httpUrlConn.setUseCaches(false);
//setConnectTimeout:设置连接主机超时(单位:毫秒)
httpUrlConn.setConnectTimeout(30000);
//setReadTimeout:设置从主机读取数据超时(单位:毫秒)
httpUrlConn.setReadTimeout(30000);
// 设定请求的方法,默认是GET
httpUrlConn.setRequestMethod("GET");
httpUrlConn.connect();
// 将返回的输入流转换成字符串
InputStream inputStream = httpUrlConn.getInputStream();
InputStreamReader inputStreamReader = new InputStreamReader(inputStream, encoding);
BufferedReader bufferedReader = new BufferedReader(inputStreamReader);
return bufferedReader;
}
// 测试用例运行失败后,收集异常日志
public static String handleException(Exception e)
{
String msg = null;
if (e instanceof InvocationTargetException) {
Throwable targetEx = ((InvocationTargetException) e)
.getTargetException();
if (targetEx != null)
{
msg = targetEx.getMessage();
}
} else {
msg = e.getMessage();
}
return msg;
}
// 获得当前页面链接
public static String getCurrentUrl (WebDriver driver) {
return driver.getCurrentUrl();
}
}
命令工具类(CommandTools.java)
package com.zuoye.qa.webtest.commands;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;
import java.util.StringTokenizer;
import org.openqa.selenium.By;
public class CommandTools {
static String packageName = CommandTools.class.getPackage().getName();
// 本项目实现的所有 command 类
static String classArray[] = {"AssertCommand", "BaseCommand", "VerifyCommand", "OtherCommand", "ZuoyeCommand", "TryCommand"};
static Map<String, String> map = new HashMap<String, String>();
public CommandTools() {
// TODO Auto-generated constructor stub
}
// 收集本项目实现的 command 命令存放到静态变量 map 里
private static void setMap() throws ClassNotFoundException {
for (String className : classArray) {
String classLongName = packageName + "." + className;
//Obtain the Class instance
Class<?> classCommand = Class.forName(classLongName);
//Get the methods
Method[] methods = classCommand.getDeclaredMethods();
//Loop through the methods and print out their names
for (Method method : methods) {
String methodName = method.getName();
if (map.containsKey(methodName)) {
System.out.println("error: method " + methodName + " in " + classLongName + " has existed in Map from " + map.get(methodName));
} else {
map.put(methodName, classLongName);
}
}
}
}
// 根据 command 命令获取它的实现类名和方法名
public static String getClassByMethod(String command) throws ClassNotFoundException {
// 静态变量 map 记录了本项目实现的 command 命令集合,如果 map 为空则开始收集
if (map.isEmpty()) {
CommandTools.setMap();
}
return map.get(command);
}
// 根据 target 转换成相应的 WebDriver By 定位元素
public static By getLocator(String target) {
if (target.startsWith("//") || target.startsWith(".//") || target.startsWith("(//")) {
target = "xpath=" + target;
}
// 使用 '=' 作为分隔符, 指定依据什么方式定位
StringTokenizer strTokens = new StringTokenizer(target, "=");
String key = strTokens.hasMoreTokens() ? strTokens.nextToken() : null;
String value = strTokens.hasMoreTokens() ? strTokens.nextToken() : null;
if (null != value && value.length() > 0) {
// value 与 value 之后的字符串合并, 实现value可以包含等号
while (strTokens.hasMoreTokens()) {
value = value + "=" + strTokens.nextToken();
}
// 根据不同的 locator 关键字使用不同的定位方式
switch (key) {
case "id":
return By.id(value);
case "name":
return By.name(value);
case "link":
return By.linkText(value);
case "plink":
return By.partialLinkText(value);
case "tag":
return By.tagName(value);
case "class":
return By.className(value);
case "css":
return By.cssSelector(value);
case "xpath":
return By.xpath(value);
default:
throw new IllegalArgumentException("Invaild format, target string is " + target);
}
} else {
throw new IllegalArgumentException("Invaild format, target string is " + target);
}
}
}
自定义命令类 (OtherCommand.java)
实现了 screenShot 截图命令
package com.zuoye.qa.webtest.commands;
import java.io.File;
import java.io.IOException;
import org.apache.commons.io.FileUtils;
import org.openqa.selenium.OutputType;
import org.openqa.selenium.TakesScreenshot;
import org.openqa.selenium.WebDriver;
/**
* 自定义命令类
*/
public class OtherCommand {
public OtherCommand() {
// TODO Auto-generated constructor stub
}
// 截图
public static void screenShot(WebDriver driver, String target, String value) throws IOException, InterruptedException {
Thread.sleep(500);
File screenshotFile = ((TakesScreenshot)driver).getScreenshotAs(OutputType.FILE);
FileUtils.copyFile(screenshotFile, new File(target));
}
}
基础命令类 (BaseCommand.java)
实现了 open click clickAndWait type sleep pause goBack sendKeys keyPress select 等操作
package com.zuoye.qa.webtest.commands;
import java.io.IOException;
import java.util.StringTokenizer;
import org.openqa.selenium.By;
import org.openqa.selenium.Keys;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.interactions.Actions;
import org.openqa.selenium.support.ui.Select;
/**
* 基础命令类
*/
public class BaseCommand {
public BaseCommand() {
// TODO Auto-generated constructor stub
}
public static void open(WebDriver driver, String target, String value) {
if (!target.startsWith("http")) {
target = "http://" + target;
}
driver.get(target);
}
public static void click(WebDriver driver, String target, String value) {
driver.findElement(CommandTools.getLocator(target)).click();
}
public static void clickAndWait(WebDriver driver, String target, String value) {
BaseCommand.click(driver, target, value);
}
public static void type(WebDriver driver, String target, String value) {
driver.findElement(CommandTools.getLocator(target)).clear();
driver.findElement(CommandTools.getLocator(target)).sendKeys(value);
}
public static void sleep(WebDriver driver, String target, String value) throws InterruptedException {
Thread.sleep(Integer.parseInt(target));
}
public static void pause(WebDriver driver, String target, String value) throws InterruptedException {
BaseCommand.sleep(driver, target, value);
}
public static void goBack(WebDriver driver, String target, String value) throws IOException, InterruptedException {
driver.navigate().back();
}
public static void sendKeys(WebDriver driver, String target, String value) {
driver.findElement(CommandTools.getLocator(target)).sendKeys(value);
}
public static void keyPress(WebDriver driver, String target, String value) {
Actions action = new Actions(driver);
switch (target.toUpperCase()) { // transform target to upper case
case "ENTER":
action.sendKeys(Keys.RETURN).perform();
break;
case "TAB":
action.sendKeys(Keys.TAB).perform();
break;
case "BACKSCAPE":
action.sendKeys(Keys.BACK_SPACE).perform();
break;
case "PAGE_DOWN":
action.sendKeys(driver.findElement(By.cssSelector("html")),Keys.PAGE_DOWN).perform();
break;
case "PAGE_UP":
action.sendKeys(driver.findElement(By.cssSelector("html")), Keys.PAGE_UP).perform();
break;
default:
break;
}
}
public static void select(WebDriver driver, String target, String value) {
Select select = new Select(driver.findElement(CommandTools.getLocator(target)));
StringTokenizer strTokens = new StringTokenizer(value, "=");
String k = strTokens.hasMoreTokens() ? strTokens.nextToken() : null;
String v = strTokens.hasMoreTokens() ? strTokens.nextToken() : null;
switch (k) {
case "index":
select.selectByIndex(Integer.parseInt(v));
break;
case "value":
select.selectByValue(v);
break;
case "label":
select.selectByVisibleText(v);
break;
default:
throw new IllegalArgumentException("Invaild format, value string is " + value);
}
}
}
断言命令类(AssertCommand.java)
实现了 assertTitle assertLocation assertElementPresent assertElementNotPresent assertText assertValue assertAttribute assertChecked assertNotChecked 等命令
package com.zuoye.qa.webtest.commands;
import org.openqa.selenium.By;
import org.openqa.selenium.WebDriver;
import static org.testng.Assert.*;
/**
* 断言命令类
*/
public class AssertCommand {
public AssertCommand() {
// TODO Auto-generated constructor stub
}
private static boolean isElementPresent(WebDriver driver, By by) {
try {
driver.findElement(by);
return true;
} catch (Exception e) {
return false;
}
}
public static void assertTitle(WebDriver driver, String target, String value) {
String str = driver.getTitle();;
assertTrue(str.matches(target));
}
public static void assertLocation(WebDriver driver, String target, String value) {
String str = driver.getCurrentUrl();
assertTrue(str.matches(target));
}
public static void assertElementPresent(WebDriver driver, String target, String value) {
assertTrue(AssertCommand.isElementPresent(driver, CommandTools.getLocator(target)));
}
public static void assertElementNotPresent(WebDriver driver, String target, String value) {
assertFalse(AssertCommand.isElementPresent(driver, CommandTools.getLocator(target)));
}
public static void assertText(WebDriver driver, String target, String value) {
String str = driver.findElement(CommandTools.getLocator(target)).getText();
assertTrue(str.matches(value));
}
public static void assertValue(WebDriver driver, String target, String value) {
String str = driver.findElement(CommandTools.getLocator(target)).getAttribute("value");
assertTrue(str.matches(value));
}
public static void assertAttribute(WebDriver driver, String target, String value) {
String attribute = target.substring(target.lastIndexOf("@") + 1);
target = target.substring(0, target.lastIndexOf("@"));
String str = driver.findElement(CommandTools.getLocator(target)).getAttribute(attribute);
assertTrue(str.matches(value));
}
public static void assertChecked(WebDriver driver, String target, String value) {
assertTrue(driver.findElement(CommandTools.getLocator(target)).isSelected());
}
public static void assertNotChecked(WebDriver driver, String target, String value) {
assertFalse(driver.findElement(CommandTools.getLocator(target)).isSelected());
}
}
验证命令类 (VerifyCommand.java)
实现了 verifyTitle verifyLocation verifyElementPresent verifyElementNotPresent verifyText verifyValue verifyAttribute verifyChecked verifyNotChecked 等命令
package com.zuoye.qa.webtest.commands;
import static org.testng.Assert.assertTrue;
import org.openqa.selenium.WebDriver;
/**
* 验证命令类
*/
public class VerifyCommand {
public VerifyCommand() {
// TODO Auto-generated constructor stub
}
public static String verifyTitle(WebDriver driver, String target, String value) {
String str = driver.getTitle();
try {
assertTrue(str.matches(target));
return "passed";
} catch (Error e) {
return "expected [" + target + "] but found [" + str + "]";
}
}
public static String verifyLocation(WebDriver driver, String target, String value) {
String str = driver.getCurrentUrl();
try {
assertTrue(str.matches(target));
return "passed";
} catch (Error e) {
return "expected [" + target + "] but found [" + str + "]";
}
}
public static String verifyElementPresent(WebDriver driver, String target, String value) {
try {
AssertCommand.assertElementPresent(driver, target, value);
return "passed";
} catch (Error e) {
return "expected element [" + target + "] is present, but not present";
}
}
public static String verifyElementNotPresent(WebDriver driver, String target, String value) {
try {
AssertCommand.assertElementNotPresent(driver, target, value);
return "passed";
} catch (Error e) {
return "expected element [" + target + "] is not present, but present";
}
}
public static String verifyText(WebDriver driver, String target, String value) {
String str = driver.findElement(CommandTools.getLocator(target)).getText();
try {
assertTrue(str.matches(value));
return "passed";
} catch (Error e) {
return "expected [" + value + "] but found [" + str + "]";
}
}
public static String verifyValue(WebDriver driver, String target, String value) {
String str = driver.findElement(CommandTools.getLocator(target)).getAttribute("value");
try {
assertTrue(str.matches(value));
return "passed";
} catch (Error e) {
return "expected [" + value + "] but found [" + str + "]";
}
}
public static String verifyAttribute(WebDriver driver, String target, String value) {
String attribute = target.substring(target.lastIndexOf("@") + 1);
target = target.substring(0, target.lastIndexOf("@"));
String str = driver.findElement(CommandTools.getLocator(target)).getAttribute(attribute);
try {
assertTrue(str.matches(value));
return "passed";
} catch (Error e) {
return "expected [" + value + "] but found [" + str + "]";
}
}
public static String verifyChecked(WebDriver driver, String target, String value) {
try {
AssertCommand.assertChecked(driver, target, value);
return "passed";
} catch (Error e) {
return "expected element [" + target + "] is checked, but not checked";
}
}
public static String verifyNotChecked(WebDriver driver, String target, String value) {
try {
AssertCommand.assertNotChecked(driver, target, value);
return "passed";
} catch (Error e) {
return "expected element [" + target + "] is not checked, but checked";
}
}
}
优点
缺点
通过 TestNG 实现并发测试,控制多个解释执行器读取多个本地测试用例文件,并在 Selenium Grid 集群里执行测试用例。
说明:
优点
缺点
搭建一个简易的测试用例存放平台,解释执行器可以通过网络获取测试用例的内容
说明:
优点
缺点
搭建一个测试用例信息管理平台 www.17test.net( ThinkPHP + MySQL ),实现如下功能:
编写 TestAgent ( Shell ), 实现如下功能:
说明:
优点
缺点
简单测试用例:
open http://www.17zuoye.com
click xpath=(//a[contains(text(),'登录')])[2]
type id=index_login_username 100xxxxxxxx
type id=index_login_password xxxxxx
click id=_a_loginForm
assertLocation (.*)/teacher/index.vpage(.*)
// 自定义的截图命令
screenShot
click //a[contains(text(),'布置新作业')]
转换为通用测试用例:
open ${index.url}
click xpath=(//a[contains(text(),'登录')])[2]
type ${base.input.username} ${teacher.user}
type ${base.input.password} ${teacher.password}
click ${base.button.login}
assertLocation (.*)/teacher/index.vpage(.*)
// 自定义的截图命令
screenShot
click //a[contains(text(),'布置新作业')]
定位元素变量配置:
${base.input.username} = id=index_login_username
${base.input.password} = id=index_login_password
${base.button.login} = id=_a_loginForm
测试环境变量配置:
${index.url} = http://www.test.17zuoye.net
${teacher.user} = testxxxxxxx
${teacher.password} = testxx
预发布环境变量配置:
${index.url} = http://www.staging.17zuoye.net
${teacher.user} = stagingxxxx
${teacher.password} = stagingxxxxx
生产环境变量配置:
${index.url} = http://www.17zuoye.com
${teacher.user} = 100xxxxxxxx
${teacher.password} = xxxxxx
测试用例执行时,根据不同的项目不同的环境执行不同的变量替换,以实现一个通用测试用例可以在多套环境执行的效果。
代码示例:test_agent.sh
#!/bin/bash
# Author: binwei.liu@17zuoye.com
DEBUG=true
PS4='+[$LINENO]'
MASTER_ABSOLUTE_PATH=$(cd `dirname $0`; pwd)
# --- env setting ---
# TC_TYPE, type测试用例类型, 1为WEB测试
TC_TYPE='1'
# 标注Agent的类型
CLIENT_TAG="WBT_$(date '+%Y%m%d_%H%M%S')"
WORKSPACE='/data'
GRID_IP='hub.17test.net'
GRID_PORT='4444'
WEBSITE_URL='http://www.17test.net'
JAVA_HOME='/usr/java/jdk1.8.0_66'
TESTNG_XML="/tmp/testng_${CLIENT_TAG}.xml"
SELENIUM_JAR="${MASTER_ABSOLUTE_PATH}/selenium-server-standalone-2.48.2.jar"
TESTNG_JAR="${MASTER_ABSOLUTE_PATH}/testng-6.9.9.jar"
TESTRUNNER_JAR="${MASTER_ABSOLUTE_PATH}/TestRunner.jar"
# --- var ---
FREE_NUM=''
declare -a TEST_RECORD_ID_LIST=''
# --- selenium grid ---
GRID_URL="http://${GRID_IP}:${GRID_PORT}"
GRID_CONSOLE_URL="${GRID_URL}/grid/console"
GRID_WEBDRIVER_HUB="${GRID_URL}/wd/hub"
# --- website ---
# 获取待测队列中执行记录的 ID 列表的链接
TEST_RECORD_IDS_URL="${WEBSITE_URL}/index.php/home/service/getqueue"
# 根据 ID 获取测试用例内容的链接
TEST_RECORD_SHOWCASE_URL="${WEBSITE_URL}/index.php/home/service/gettestcase"
# 设置测试用例执行结果的链接
TEST_RECORD_SUBMIT_RESULT_URL="${WEBSITE_URL}/index.php/home/service/settestresult"
# 记录测试结果的文件夹
TEST_RESULT_DIR="${WORKSPACE}/webtest/results"
# --- java settings ---
CLASSPATH=".:$JAVA_HOME/jre/lib/rt.jar:$JAVA_HOME/lib/dt.jar:$JAVA_HOME/lib/tools.jar"
PATH="$PATH:$JAVA_HOME/bin"
# Fixme
function self_check () {
:
# check if WEBSITE_URL is available
# check if GRID_URL is available
}
# 获取 Selenium Grid 空闲的浏览器数量
function curl_grid_free_session_num () {
echo GRID_CONSOLE_URL=${GRID_CONSOLE_URL}
FREE_NUM=$( curl -sS ${GRID_CONSOLE_URL} | grep 'seleniumProtocol=WebDriver' | grep 'browserName=firefox' | wc -l )
echo FREE_NUM=${FREE_NUM}
}
# 获取待测队列中执行记录的 ID 列表
function curl_caserun_ids () {
# type : 测试用例类型, 1 为WEB测试
echo "url=${TEST_RECORD_IDS_URL}?type=${TC_TYPE}&client=${CLIENT_TAG}&count=${FREE_NUM}"
TEST_RECORD_ID_LIST=($(curl -sS "${TEST_RECORD_IDS_URL}?type=${TC_TYPE}&client=${CLIENT_TAG}&count=${FREE_NUM}"))
TEST_RECORD_ID_LIST=(${TEST_RECORD_ID_LIST//,/ })
}
# 生成 testng.xml
function create_testng_xml () {
cat >> ${TESTNG_XML} <<EOF
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE suite SYSTEM "http://testng.org/testng-1.0.dtd">
<suite name="Suite" parallel="tests" thread-count="${#TEST_RECORD_ID_LIST[@]}">
<parameter name="seleniumGridHub" value="${GRID_WEBDRIVER_HUB}" />
EOF
for _id in ${TEST_RECORD_ID_LIST[@]} ; do
echo _id=${_id}
cat >> ${TESTNG_XML} <<EOF
<test name="testrecord_${_id}">
<parameter name="showTestcaseUrl" value="${TEST_RECORD_SHOWCASE_URL}/tr_id/${_id}" />
<parameter name="submitTestresultUrl" value="${TEST_RECORD_SUBMIT_RESULT_URL}/tr_id/${_id}/result/" />
<parameter name="logFile" value="${TEST_RESULT_DIR}/${_id}/runner.log" />
<parameter name="screenshotDir" value="${TEST_RESULT_DIR}/${_id}/" />
<classes>
<class name="com.zuoye.qa.webtest.TestRunner" />
</classes>
</test> <!-- Test -->
EOF
done
cat >> ${TESTNG_XML} <<EOF
</suite> <!-- Suite -->
EOF
cat ${TESTNG_XML}
}
# 执行测试
function test_runner () {
java -classpath "${SELENIUM_JAR}:${TESTNG_JAR}:${TESTRUNNER_JAR}" org.testng.TestNG ${TESTNG_XML}
}
# --- main ---
function main () {
# 获取 Selenium Grid 空闲的浏览器数量
curl_grid_free_session_num
if test ${FREE_NUM} -le 0 ; then
echo "Info: Selenium Grid Hub no free sessions for more test. Exit"
exit 1
fi
# 获取待测队列中执行记录的 ID 列表
curl_caserun_ids
if test "X${TEST_RECORD_ID_LIST}" == "Xnone" ; then
echo "Info: No queuing testcases found to run. Exit"
exit 1
fi
# 生成 testng.xml
create_testng_xml
# 执行测试
test_runner
}
main
TestAgent 生成的 testng.xml:
参数说明:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE suite SYSTEM "http://testng.org/testng-1.0.dtd">
<suite name="Suite" parallel="tests" thread-count="10">
<parameter name="seleniumGridHub" value="http://hub.17test.net:4444/wd/hub" />
<test name="testrecord_102149">
<parameter name="showTestcaseUrl" value="http://www.17test.net/index.php/home/service/gettestcase/tr_id/102149" />
<parameter name="submitTestresultUrl" value="http://www.17test.net/index.php/home/service/settestresult/tr_id/102149/result/" />
<parameter name="logFile" value="/data/webtest/results/102149/runner.log" />
<parameter name="screenshotDir" value="/data/webtest/results/102149/" />
<classes>
<class name="com.zuoye.qa.webtest.TestRunner" />
</classes>
</test> <!-- Test -->
<test name="testrecord_102150">
<parameter name="showTestcaseUrl" value="http://www.17test.net/index.php/home/service/gettestcase/tr_id/102150" />
<parameter name="submitTestresultUrl" value="http://www.17test.net/index.php/home/service/settestresult/tr_id/102150/result/" />
<parameter name="logFile" value="/data/webtest/results/102150/runner.log" />
<parameter name="screenshotDir" value="/data/webtest/results/102150/" />
<classes>
<class name="com.zuoye.qa.webtest.TestRunner" />
</classes>
</test> <!-- Test -->
<test name="testrecord_102153">
<parameter name="showTestcaseUrl" value="http://www.17test.net/index.php/home/service/gettestcase/tr_id/102153" />
<parameter name="submitTestresultUrl" value="http://www.17test.net/index.php/home/service/settestresult/tr_id/102153/result/" />
<parameter name="logFile" value="/data/webtest/results/102153/runner.log" />
<parameter name="screenshotDir" value="/data/webtest/results/102153/" />
<classes>
<class name="com.zuoye.qa.webtest.TestRunner" />
</classes>
</test> <!-- Test -->
</suite> <!-- Suite -->
执行日志:
测试报告: