Selenium 基于 Selenium IDE 录制的测试用例逐步实现关键字驱动的测试平台

刘彬伟 · 2018年06月01日 · 3010 次阅读

内容简介

本文主要记录了笔者从使用 Selenium IDE 录制/回放测试用例开始,过渡到用编写代码的方式实现测试用例(非 Page Object 方式),并用命令行的方式在 Linux 平台执行测试用例,包括在 Linux 平台搭建 Selenium Grid ,然后实现了一个脚本解释器,直接解释执行由 Selenium IDE 录制的三段式测试用例,最后开发了一个测试用例信息管理网站,管理和并发执行测试用例,并生成测试报告发送邮件。

本文共十个阶段,前五个阶段是 Selenium 的基础知识,主要介绍了 Selenium IDE、 Selenium Grid 和 TestNG 的用法,以及如何在 Linux 平台上搭建执行环境和并发运行测试用例, 为后面阶段实现分布式测试做好准备。
后五个阶段主要介绍了如何实现一个测试用例解释执行器,并通过搭建网站的方式管理和运行测试用例,并对这个网站进行了优化。

项目背景

这是一个网页自动化测试平台,2016 年 1 月份开始编写,断断续续大概 3 个月基本完成,后续也做过一些小的调整和修复。
当时笔者也是刚开始接触学习 Selenium, 想把当时的学习路线、管理平台实现以及遇到的一些问题和解决方法分享给大家,希望能对初学者有一定的帮助和参考。
本文采用的是回忆的写法,因为时间久远,一些开始阶段的详细步骤都已经忘记了,所以写得比较简练,后面阶段会越来越详细。

实现目标

  1. 动静分离,将测试用例编写和测试用例实现相分离,便于熟悉业务的同学编写测试用例,熟悉开发的同学实现测试用例。
  2. 降低自动化测试用例的编写复杂度和变更难度,少写代码或者不写代码。
  3. 可以在 Linux 环境上执行,同时测试用例可以并发执行。

涉及技术

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(),'布置新作业')]

优点

  • 简单方便,快速录制编写测试用例

缺点

  • 只能使用本机的 Firefox 浏览器回放(Katalon Recorder 可以在 Chrome 浏览器上录制/回放)
  • 不能使用命令行执行测试用例

第二阶段 测试用例脚本转换成 JAVA + TestNG 代码

使用 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;
    }
  }
}

优点

  • 可以使用命令行的方式执行测试用例。
  • 可以通过修改代码里的浏览器 driver 在多种浏览器上运行。

缺点

  • 只能在本机执行,操作系统必须有图形界面

第三阶段 Linux 服务器上执行测试用例

在已经安装 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 集群

在 Linux 环境下,搭建 Selenium Grid 集群,可以使用 Docker 的方式搭建,方便快捷。

说明:

  • Docker 内也是通过 xvfb 实现的虚拟界面执行测试用例。
  • Selenium Grid 只是提供了多平台多浏览器的测试环境,辅助我们进行并发测试,但并不能发起并发测试。
  • 示例中使用的是灵雀云的镜像服务,当时镜像版本是 2.53.0
  • 目前 Selenium 最新版是 3.12.0,部署文档参见:https://github.com/SeleniumHQ/docker-selenium

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()

优点

  • 可以在不同的操作系统上不同的浏览器上或者相同浏览器的不同版本上运行测试用例。( 注:需要额外搭建 Windows 平台和 Mac 平台的 Selenium Node 加入到该集群)
  • 执行测试用例命令的机器和实际运行的机器相分离。

缺点

  • 不能看到测试用例运行时的图形界面。

第五阶段 使用 TestNG 实现并发测试

使用 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 配置文件路径

优点

  • 并发执行测试用例

缺点

  • 手动编写 testng.xml 配置文件

第六阶段 实现测试用例解释执行器(稍难)

用 Java 语言实现一个测试用例解释执行器,逐行解释执行存放在本地文件里的 Selenium IDE 录制的测试用例脚本。

说明:

  • 在测试用例解释执行器主引擎代码里,应该是读取本地文件里的测试用例并执行,因为时间久远,忘记了实现方法,所以文章中使用的是从网络中获取测试用例的方法。
  • WebDriver driver 指定的是第四阶段实现的 Selenium Grid Hub 地址

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";
        }
    }
}

优点

  • 可以直接执行 Selenium IDE 录制的测试用例脚本
  • 支持自定义的截图命令
  • 远程收集执行过程中的异常日志

缺点

  • 不支持程序控制语句,比如:条件、循环等
  • 没有实现复杂的操作命令

第七阶段(过渡)

通过 TestNG 实现并发测试,控制多个解释执行器读取多个本地测试用例文件,并在 Selenium Grid 集群里执行测试用例。

说明:

  • 本阶段属于过渡阶段,没有出现新的知识和方法,只是对过去几个阶段的知识的组合。
  • TestNG 的配置文件里支持配置变量,可以把测试用例的文件地址写在 testng.xml 里,作为变量传递给解释执行器
  • 本阶段未提供参考代码

优点

  • 实现分布式测试,可以大规模并发执行测试用例

缺点

  • 测试用例文件繁多,不方便管理和变更
  • 测试日志繁多,不利于收集和查看
  • 需要手动编写 testng.xml
  • 需要手动执行测试用例

第八阶段 搭建简易测试用例存放平台

搭建一个简易的测试用例存放平台,解释执行器可以通过网络获取测试用例的内容

说明:

  • 把测试用例的链接写在 testng.xml 里,作为变量传递给解释执行器
  • 本阶段未提供参考代码

优点

  • 通过网页的方式编写、变更和保存测试用例
  • 方便分类管理测试用例

缺点

  • 无法方便地收集和查看执行日志和截图

第九阶段 搭建测试用例信息管理平台(稍难)

  1. 搭建一个测试用例信息管理平台 www.17test.net( ThinkPHP + MySQL ),实现如下功能:

    • 管理测试用例
    • 定义测试轮期
    • 生成要执行的测试用例记录列表
    • 收集执行的日志和截图
    • 生成测试报告并发送邮件
  2. 编写 TestAgent ( Shell ), 实现如下功能:

    • 每分钟执行一次
    • 访问测试用例信息管理平台,获取待执行的测试用例列表
    • 生成 testng.xml
    • 并发执行测试用例

说明:

  • TestAgent 和测试用例信息管理平台运行在同一个服务器上,方便通过管理平台查看由 TestAgent 收集的执行日志和截图
  • TestAgent ( Shell ) 脚本内容将在下一个阶段优化后展示

优点

  • 方便快捷地管理测试用例和查看执行日志和截图
  • TestAgent 自动生成 testng.xml 并发执行测试用例

缺点

  • 同一个业务场景需要针对三套环境编三个测试用例(三套环境分别是:测试环境、预发布环境、生产环境)
  • 测试用例没有优先级,重要的测试用例不能优先执行

第十阶段(优化)

  1. 测试用例信息管理平台优化
    • 支持多级前置用例,即前置用例里仍可以包含前置用例
    • 一个测试用例可以在多套环境里执行,做到测试环境无关性
    • 设置测试用例执行优先级,后发起的高优先级测试用例仍可优先执行, 比如:冒烟测试用例会优先于功能测试用例执行

简单测试用例:

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

测试用例执行时,根据不同的项目不同的环境执行不同的变量替换,以实现一个通用测试用例可以在多套环境执行的效果。

  1. TestAgent 优化
    • 收集 Selenium Grid 集群里空闲的浏览器数量
    • 访问测试用例信息管理平台,获取待执行的测试用例列表,获取数不多于空闲的浏览器数量

代码示例: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:

参数说明:

  • showTestcaseUrl:获取测试用例内容的链接
  • submitTestresultUrl:提交执行结果的链接
  • logFile:存放运行日志的文件
  • screenshotDir:保存截图的目录
<?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 -->

平台展示

执行日志:

测试报告:

如果觉得我的文章对您有用,请随意打赏。您的支持将鼓励我继续创作!
暂无回复。
需要 登录 后方可回复, 如果你还没有账号请点击这里 注册