第3章自动化测试实战——京东系统质量保障技术实战(1)

发表于:2017-11-03 15:44

字体: | 上一篇 | 下一篇 | 我要投稿

 作者:商城研发POP平台    来源:电子工业出版社

  第3章自动化测试实战
  3.1 WebUI 实战
  我们都知道Web 页面的自动化测试大多是通过Selenium 或者QTP 这样的工具或者框架来完成的。QTP 是存在时间非常久的一款自动化测试工具。它基于关键字驱动,非常容易上手。但是它的缺点也很明显,就是对于Web 页面的测试显得力不从心。这是由它自身原理所决定的,本章就不讲述QTP 工具了。下面主要讲述Selenium 框架。它是由Thoughworks 开发的一款自动化测试框架。现在有两个大的版本,基本上我们现在听到的说法,都是Selenium WebDriver 或者直接是WebDriver。这其中的区别,后面会有详细的叙述。
  笔者也是使用Selenium WebDriver 框架进行WebUI 测试的。通过Java 语言编写用例代码,构建在Maven 工程之上,利用TestNG 单元测试框架组织脚本,最后运行在Jenkins 之上。通过这样的组织方式,灵活地完成用例代码编写,达到“一次编写,多次运行”的目标。
  3.1.1 Selenium
  笔者使用的是Selenium WebDriver,即Selenium 2.0。WebDriver 的API 设计非常清晰,适合Web 项目,这样的优点是笔者采用其框架的理由。因为Java 语言非常成熟,所以使用Java 作为编写用例代码的语言。
  在用例管理上,笔者使用了GIT 进行源代码的管理。其实,笔者也用过一段时间的SVN。后来,因为GIT 的流行,笔者的用例代码也就自然地迁移到了GIT 之上。在进行多人同时开发时,协作方式可以是这样的:一个应用会对应一个GIT 项目,这个项目中只有测试代码,与应用的代码是彼此分隔的。一个GIT 项目可能有多个工程师进行自己模块的测试代码编写,通过不同的功能进行区分。这样大家就能够在同一个项目中进行代码编写了。而其中的公共代码,可以供每个成员使用。当用例完成之后,编写者会发起一个Merge Request,也就是用例的评审。评审的目的是为了让用例代码质量更好,覆盖功能更加全面。而评审的参与者是编写者、开发者和自动化专家。大家会通过面对面的形式对代码进行各自角度的建议,然后把评审意见记录到这个Merge Request 中。当代码评审完成并把更新的代码提交后,项目的负责人将接受这个Merge Request,也就是把代码合并到了一个稳定的分支中。
  TestNG 的作用是为了让用例分成不同的场景运行。用例编写者会创建一系列的XML 文件驱动用例的执行。这些XML 文件是对用例进行一个场景化的划分。例如,有冒烟测试的用例集构成的XML 文件,作用就是对应用进行冒烟测试;有基本核心功能测试的用例集,作用就是对应用进行核心功能测试;也有某个新功能的用例集,作用是新功能的测试。除了场景化,笔者还对报表的呈现进行了定制开发,开发了一款类似于ReportNG 的报表功能。它不但能够呈现好看的运行报告,还可以将报告通过邮件的方式发送给订阅者。这样,在用例集执行完毕之后,就能通过邮件即时收到最新的报告了。
  在使用上,还有一个部分非常重要,就是Jenkins。我们知道Jenkins 是一款持续集成工具。可以持续部署应用环境。笔者利用Jenkins 进行测试项目的构建。每天定时运行测试项目,获得测试报告,这样就可以以天为维度进行应用质量的跟踪。
  综上,笔者使用了Selenium WebDriver + Java + TestNG + GIT + Jenkins 这样的技术栈。目前,各方面运行稳定,用例脚本编写灵活,基本满足需要。上面是Selenium 的使用情况,下面来讨论一下Selenium 技术。接下来会围绕Selenium 原理和高级技巧这两个话题进行讲述。
   1.Selenium 原理
Selenium 是一种Web 测试框架,它搭建了验证Web 应用程序的新途径。与大多数尝试模拟 HTTP 请求的Web 测试工具不同,Selenium 执行Web 测试时,就仿佛它本身就是浏览器。当运行自动的Selenium 测试时,该框架将启动一个浏览器,并通过测试用例中描述的步骤实现驱动浏览器,用户将使用这种方式与应用程序交互。
  Selenium 分为两个时代:
  (1)Selenium RC - Selenium 1。早期的Selenium 使用的是JavaScript 注入技术与浏览器打交道,需要Selenium RC 启动一个Server,将操作Web 元素的API 调用转化为可执行的JavaScript 脚本,在Selenium 内核启动浏览器之后注入此JavaScript脚本。
  开发过Web 应用的人都知道,JavaScript 可以获取并调用页面的任何元素,自由地进行操作。由此才实现了Selenium 的目的——自动化Web 操作。这种JavaScript注入技术的缺点是速度不理想,而且稳定性大大依赖于Selenium 内核对API 翻译成的JavaScript 的质量高低。
  (2)WebDriver - Selenium 2。当Selenium 2.x 提出了WebDriver 的概念之后,它提供了完全不同的一种方式与浏览器交互。那就是利用浏览器原生的API,封装成一套更加面向对象的Selenium WebDriver API,直接操作浏览器页面里的元素,甚至操作浏览器本身(截屏、窗口大小、启动、关闭、安装插件、配置证书之类的)。由于使用的是浏览器原生的API,速度大大提高,而且调用的稳定性交给了浏览器厂商本身,显然更加科学。然而带来的一些副作用就是,不同的浏览器厂商,对Web元素的操作和呈现会有一些差异,这就直接导致了Selenium WebDriver 要根据浏览器厂商不同,而提供不同的实现。例如Firefox 就有专门的Firefox Driver,Chrome也有专门的Chrome Driver 等(甚至包括了Android Driver 和iOS Driver)。
  WebDriver Wire 协议是通用的,也就是说不管是Firefox Driver 还是ChromeDriver,启动之后都会在某一个端口启动基于这套协议的Web Service。例如,FirefoxDriver 初始化成功之后,默认会从http://localhost:7055 开始,而Chrome Driver 则是从http://localhost:46350 开始的。接下来,我们调用WebDriver 的任何API,都需要借助CommandExecutor 发送命令,实际上是发送一个HTTP 请求给监听端口上的Web Service。在我们的HTTP 请求的body 中,会以WebDriver Wire 协议规定的JSON格式的字符串来告诉Selenium 我们希望浏览器接下来做什么事情。
  在我们实例化一个WebDriver 的过程中,Selenium 首先会确认浏览器的NativeComponent(本地组件)是否存在可用且匹配的版本。接着就在目标浏览器里启动一整套Web Service,这套Web Service 使用了Selenium 自己设计定义的协议,名字叫作The WebDriver Wire Protocol。这套协议非常之强大,几乎可以操作浏览器做任何事情,包括打开、关闭、最大化、最小化、元素定位、元素点击、上传文件等。
  Selenium 架构实际上由两个逻辑实体组成:编写的代码和能够简化与测试中应用程序交互的Selenium 服务器。要成功地执行测试,必须要启动并运行Selenium 服务器实例及要测试的应用程序。
  不同的浏览器都有对应的驱动程序,通过启动这个驱动程序就可以启动浏览器了。我们都知道,在用Selenium 测试IE、Chrome 等浏览器时,需要加载浏览器所对应的驱动程序。而Firefox 浏览器并不需要,这是因为Firefox 内置了这样的驱动程序。
  通过研究源码,可以发现更多东西。下面以Firefox Driver 为例,介绍整个过程。当测试脚本启动Firefox 时,Selenium WebDriver 会在新线程中启动Firefox 浏览器。如果测试脚本中指定了Firefox 的profile,那么就以该profile 启动,否则就新建一个profile,并启动Firefox。
  Firefox 一般是以-no-remote 方法启动。启动后Selenium WebDriver 会将Firefox绑定到特定的端口。之所以要作为服务器端存在,是因为WebDriver 在启动浏览器的同时启动了一套操作浏览器页面的Web Service 服务。客户端就是通过访问这些Web Service 来与服务器端交互的。
  测试脚本会创建一个会话,在该会话中通过HTTP 请求向服务端发送restful 请求,服务端解析请求,完成相应操作并返回响应;客户端接受响应,并做出相应的操作。
  测试代码包括打开浏览器、跳转到特定的URL 等操作,这些操作是以HTTP 请求的方式发送给被测试浏览器的,浏览器接受请求,然后执行相应操作,并在响应中返回执行状态、返回值等信息。
  举个实际的例子,下面代码的作用是“命令”Firefox 转跳到Google 主页:
  driver = new FirefoxDriver();
  driver.get("http://www.google.com");
  在执行driver.get("http://www.google.com") 这句代码时,测试代码向服务端发送了如下的请求:
  POST session/ 00754f4f-394c-46b4-93b8-eadae71b4dbb/url
  post_data {"url":"http://www.google.com"}
  通过POST 的方式请求localhost:port/hub/session/session_id/url 地址,请求浏览器完成跳转URL 的操作。之后返回的响应是:
  {"name":"get","sessionId":"00754f4f-394c-46b4-93b8-eadae71b4dbb","status":0,"value":""}
  该响应中包含如下信息:
  ●name:服务端实现方法的名称;
  ●sessionId:当前session 的id;
  ●status:请求执行的状态码,非0 表示未正确执行。这里是0,表示正常;
  ●value:请求的返回值,这里返回值为空。如果客户端调用title 接口,则该值应该是当前页面的title。
  如果客户端发送的请求是定位某个特定的页面元素,则响应的返回值可能是这样的:
  {"name":"findElement","sessionId":"00754f4f-394c-46b4-93b8-eadae71b4dbb","status":0,"value":{"ELEMENT":"{08f42afa-a5d6-40f6-836d-9122b535d6ba}"}}
  这里name、sessionId、status 与上面的例子是差不多的,区别是该请求的返回值是ELEMENT:{ 08f42afa-a5d6-40f6-836d-9122b535d6ba },表示定位到元素的id,通过该id,客户端可以发送如click 之类的请求与服务器端进行交互。
  浏览器实现了WebDriver 的统一接口,这样客户端就可以通过统一的restful 接口去进行浏览器的自动化操作。目前WebDriver 支持IE、Chrome、Firefox、Opera等主流浏览器,其主要原因是这些浏览器实现了WebDriver 约定的各种接口。
  2.高级使用技巧之智能等待
  为什么需要等待?页面中有些元素并不是一次加载完成的。可能是所有元素都加载完毕后才显示出来。又或者是一个异步的加载,虽然页面早已经加载完毕,但是某一部分是异步加载,等数据返回之后,才加载这部分。随着AJAX 的流行,这样的情况会经常碰到。
当我们碰到这样的情况后,难道只能用Thread.sleep(5000)这样的方式解决吗?
  这句话的意思是让线程等待5 秒。如果5 秒之后期待的元素还是没有加载完毕呢?又或者元素在不到5 秒时就加载完毕,我们也需要等待5 秒?这当然是不科学的。所以Selenium 提供了两种方式解决这个问题:一种是显式等待,一种是隐式等待。
  为什么先说隐式等待?因为不推荐使用,但是我们需要区别显式和隐式。为什么不推荐?因为不够灵活。下面请看:
  当使用了隐式等待执行测试时,如果WebDriver 没有在DOM(Document ObjectModel,文档对象模型)中找到元素,那么将继续等待。当超出设定时间后,则抛出找不到元素的异常。换句话说,当查找元素或者元素并没有立即出现时,隐式等待将停留一段时间再查找DOM。这个等待时间,是用户自己设置的,默认时间为0。一旦设置了隐式等待,它将应用至整个WebDriver 对象实例的生命周期中。
  隐式等待的一个缺点是会让一个正常响应的应用的测试变慢,它将会在寻找每个元素时进行等待,这样无形中就增加了整个测试的执行时间。当然,它的用法还是非常简单的,如下所示。
  启动一个Driver,例如FirefoxDriver。
  设置隐式等待时间:
  driver.manage().timeouts().implicitlyWait(10, TimeUnit.SECONDS);
  显式等待只在需要同步的地方执行,而不影响脚本其他地方。Selenium 通过WebDriverWait 和ExpectedCondition 类执行显式等待。使用方式如代码示例3.1.1 所示。
  代码示例3.1.1
  //设置等待时间为10 秒
  WebDriverWait wait = new WebDriverWait(driver, 10);//等待直到符合元素的文本内容
  出现
  //WebDriverWait 每500 毫秒调用一次ExpectedCondition 直到正确的返回值
  //可见这样的好处就是随时控制所需要等待的地方,更加精确地控制所需要的条件
  wait.until(ExpectedConditions.textToBePresentInElementLocated(By.id("pageC
  ontent"), "Nunc nibh tortor"));
  ExpectedCondition 类提供了一系列预定义好的条件来等待。下面列出了常用条件,如表3.1.1 所示。
  3.1.2 PageFactory
  PageFactory 主要是为了支持PageObjects 的设计模式而做的扩展。那什么是PageObjects 呢?
  在PageObjects 产生之前,我们是怎么编写WebDriver 代码的呢?大多数写的代码是一个流程,也就是面向过程的编程,整个流程就是在一个方法里。这样的好处是一气呵成、逻辑清晰。但是坏处也非常明显,就是无法复用。什么情况下需要复用?笔者的理解是任何情况。例如,网站的登录,写每个流程之前肯定都需要登录。所以登录可以变成一个独立模块,后面的流程在需要时调用就可以了。这只是一个简单的例子,而实战中我们还会碰到其他的公共行为可以抽象为一个独立模块的情况。所以,我们需要用一种方式,能够更好地将各个模块独立。这就是PageObjects能够流行的关键。
  什么是PageObjects?笔者的理解就是将一个页面当作一个对象,通过对对象的包装,提供一系列的操作方式,然后有一个驱动程序对包装好的对象进行操纵。整个过程就是PageObjects 模式。怎么包装呢?其实就是把页面元素操作封装到对象里面,而暴露元素操作的过程。这个过程是高度定制化的。开发者可以任意组织,只是需要维护好页面关系即可。所以整个开发的动作可以分成两部分,一部分是页面的包装,一部分是对页面的操纵。这种工作模式的建立,就像流水线一样,分工明确,职责分明,可以极大地提高开发效率。
  具体做法可以用一个例子来说明,PageObjects 模式登录脚本如代码示例3.1.2所示。
  代码示例3.1.2
  public class LoginPage {
  private final WebDriver driver;
  public LoginPage(WebDriver driver) {
  this.driver = driver;
  // Check that we're on the right page
  if (!"Login".equals(driver.getTitle())) {
  // Alternatively, we could navigate to the login page, perhaps logging out
  first throw new IllegalStateException("This is not the login page");
  }
  }
  // The login page contains several HTML elements that will be represented as
  WebElements
  // The locators for these elements should only be defined once.
  By usernameLocator = By.id("username");
  By passwordLocator = By.id("passwd");
  By loginButtonLocator = By.id("login");
  // The login page allows the user to type their username into the username
  field public LoginPage typeUsername(String username) {
  // This is the only place that "knows" how to enter a username
  driver.findElement(usernameLocator).sendKeys(username);
  // Return the current page object as this action doesn't navigate to a page
  //represented by another PageObject
  return this;
  }
  // The login page allows the user to type their password into the password
  field public LoginPage typePassword(String password) {
  // This is the only place that "knows" how to enter a password
  driver.findElement(passwordLocator).sendKeys(password);
  // Return the current page object as this action doesn't navigate to a page
  //represented by another PageObject
  return this;
  }
  // The login page allows the user to submit the login form
  public HomePage submitLogin() {
  // This is the only place that submits the login form and expects the
  // destination to be the home page
  // A seperate method should be created for the instance of clicking login
  // whilst expecting a login failure
  driver.findElement(loginButtonLocator).submit();
  // Return a new page object representing the destination. Should the login
  // page ever
  // go somewhere else (for example, a legal disclaimer) then changing the
  // method signature
  // for this method will mean that all tests that rely on this behaviour won't
  // compile.
  return new HomePage(driver);
  }
  // The login page allows the user to submit the login form knowing that an
  // invalid username and / or password were entered
  public LoginPage submitLoginExpectingFailure() {
  // This is the only place that submits the login form and expects the
  // destination to be the login page due to login failure.
  driver.findElement(loginButtonLocator).submit();
  // Return a new page object representing the destination. Should the user
  // ever be navigated to the home page after submiting a login with credentials
  // expected to fail login, the script will fail when it attempts to instantiate
  // the LoginPage PageObject.
  return new LoginPage(driver);
  }
  // Conceptually, the login page offers the user the service of being able to "log
  // into"
  // the application using a user name and password.
  public HomePage loginAs(String username, String password) {
  // The PageObject methods that enter username, password & submit login have
  // already defined and should not be repeated here
  typeUsername(username);
  typePassword(password);
  return submitLogin();
  }
  }
  从上面的代码中我们可以看到LoginPage 对象是登录页面,HomePage 对象是登录后跳转的首页。LoginPage 对外暴露typeUsername 方法,即输入用户名,typePassword 方法即输入密码,submitLogin 方法即提交登录表单转向首页。这样,其他调用者只需要实例化LoginPage,然后调用其暴露的方法即可。
  为了更好地支持PageObjects 模式,WebDriver 库中引入了一个工厂类。
  当我们运行示例时,PageFactory 将在页面上搜索与类中的WebElement 的字段名匹配的元素。它会通过查找具有匹配ID 属性的元素来实现。如果失败,那么PageFactory 会选择通过其“name”属性的值搜索元素。如代码示例3.1.3 所示的Google主页脚本。
  代码示例3.1.3
  package org.openqa.selenium.example;
  import org.openqa.selenium.By;
  import org.openqa.selenium.support.FindBy;
  import org.openqa.selenium.support.How;
  import org.openqa.selenium.WebElement;
  public class GoogleSearchPage {
  // The element is now looked up using the name attribute
  @FindBy(how = How.NAME, using = "q")
  private WebElement searchBox;
  public void searchFor(String text) {
  // We continue using the element just as before
  searchBox.sendKeys(text);
  searchBox.submit();
  }
  }

本文选自《京东系统质量保障技术实战》第三章,本站经机械工业出版社和作者的授权。
版权声明:51Testing软件测试网获机械工业出版社和作者授权连载本书部分章节。
任何个人或单位未获得明确的书面许可,不得对本文内容复制、转载或进行镜像,否则将追究法律责任。
21/212>
《2023软件测试行业现状调查报告》独家发布~

关注51Testing

联系我们

快捷面板 站点地图 联系我们 广告服务 关于我们 站长统计 发展历程

法律顾问:上海兰迪律师事务所 项棋律师
版权所有 上海博为峰软件技术股份有限公司 Copyright©51testing.com 2003-2024
投诉及意见反馈:webmaster@51testing.com; 业务联系:service@51testing.com 021-64471599-8017

沪ICP备05003035号

沪公网安备 31010102002173号