自动化测试(9) | Selenium Java 进阶

WebDriver 进阶

欢迎阅读WebDriver进阶讲义。本篇讲义将会重点介绍Selenium WebDriver API的重点使用方法,以及使用模块化和参数化进行自动化测试的设计。

WebDriver API 进阶使用

元素定位

从之前的讲义和学习中,我们知道,WebDriver API的调用以及自动化测试,务必从页面元素的定位开始,那么回顾之前的内容,WebDriver提供了一系列的定位符以便使用元素定位方法。常见的定位符有以下几种:

  • id
  • name
  • class name
  • tag
  • link text
  • partial link text
  • xpath
  • css selector

那么我们以下的操作将会基于上述的定位符进行定位操作。

对于元素的定位,WebDriver API可以通过定位简单的元素和一组元素来操作。在这里,我们需要告诉Selenium如何去找元素,以至于他可以充分的模拟用户行为,或者通过查看元素的属性和状态,以便我们执行一系列的检查。

在Selenium2中,WebDriver提供了多种多样的findElement(by.)方法在一个网页里面查找元素。这些方法通过提供过滤标准来定位元素。当然WebDriver也提供了同样多种多样的findElements(by.)的方式去定位多个元素。

Selenium2提供的8个findElement(by.)方法去定位元素。在这里我们来具体查看每个方法的详细使用方式。下面的表格将会列出这些具体的方法:

方法Method 描述Description 参数Argument 示例Example
id 该方法通过ID的属性值去定位查找单个元素 id: 需要被查找的元素的ID findElement(by.id("search"))
name 该方法通过name的属性值去定位查找单个元素 name: 需要被查找的元素的名称 findElement(by.name("q"))
class name 该方法通过class的名称值去定位查找单个元素 class_name: 需要被查找的元素的类名 findElement(by.className("input-text"))
tag_name 该方法通过tag的名称值去定位查找单个元素 tag: 需要被查找的元素的标签名称 findElement(by.tagName("input"))
link_text 该方法通过链接文字去定位查找单个元素 link_text: 需要被查找的元素的链接文字 findElement(by.linkText("Log In"))
partial_link_text 该方法通过部分链接文字去定位查找单个元素 link_text: 需要被查找的元素的部分链接文字 findElement(by.partialLinkText("Long"))
xpath 该方法通过XPath的值去定位查找单个元素 xpath: 需要被查找的元素的xpath findElement(by.xpath("//*[@id='xx']/a"))
css_selector 该方法通过CSS选择器去定位查找单个元素 css_selector: 需要被查找的元素的ID findElement(by.cssSelector("#search"))

接下来的列表将会详细展示find_elements_by的方法集合。这些方法依据匹配的具体标准返回一系列的元素。

方法Method 描述Description 参数Argument 示例Example
id 该方法通过ID的属性值去定位查找多个元素 id: 需要被查找的元素的ID findElements(by.id("search"))
name 该方法通过name的属性值去定位查找多个元素 name: 需要被查找的元素的名称 findElements(by.name("q"))
class_name 该方法通过class的名称值去定位查找多个元素 class_name: 需要被查找的元素的类名 findElements(by.className("input-text"))
tag_name 该方法通过tag的名称值去定位查找多个元素 tag: 需要被查找的元素的标签名称 findElements(by.tagName("input"))
link_text 该方法通过链接文字去定位查找多个元素 link_text: 需要被查找的元素的链接文字 findElements(by.linkText("Log In"))
partial_link_text 该方法通过部分链接文字去定位查找多个元素 link_text: 需要被查找的元素的部分链接文字 findElements(by.partialLinkText("Long"))
xpath 该方法通过XPath的值去定位查找多个元素 xpath: 需要被查找的元素的xpath findElements(by.xpath("//div[contains(@class,'list')]"))
css_selector 该方法通过CSS选择器去定位查找多个元素 css_selector: 需要被查找的元素的ID findElements(by.cssSelector(".input_class"))

依据ID查找

请查看如下HTML的代码,以便实现通过ID的属性值去定义一个查找文本框的查找:

<input id="search" type="text" name="q" value=""
       class="input-text" maxlength="128" autocomplete="off"/>

根据上述代码,这里我们使用findElement(By.id())的方法去查找搜索框并且检查它的最大长度maxlength属性。我们通过传递ID的属性值作为参数去查找,参考如下的代码示例:

void testSearchTextFieldMaxLength(){
    // get the search textbox
    WebElement searchField = driver.findElement(By.id("search"))
    // check maxlength attribute is set to 128
    assertEqual("128", searchField.getAttribute("maxlength"))
}

如果使用findElement(By.id())方法,将会返回所有的具有相同ID属性值的一系列元素。

依据名称name查找

这里还是根据上述ID查找的HTML代码,使用findElement(By.id())的方法进行查找。参考如下的代码示例:

// get the search textbox
searchField = self.driver.findElement(By.name("q"))

同样,如果使用findElements(By.name())方法,将会返回所有的具有相同name属性值的一系列元素。

依据class name查找

除了上述的ID和name的方式查找,我们还可以使用class name的方式进行查找和定位。

事实上,通过ID,name或者类名class name查找元素是最提倡推荐的和最快的方式。当然Selenium2 WebDriver也提供了一些其他的方式,在上述三类方式条件不足,查找无效的时候,可以通过这些其他方式来查找。这些方式将会在后续的内容中讲述。

请查看如下的HTML代码,通过改代码进行练习和理解.

<button type="submit" title="Search" class="button">
  <span><span>Search</span></span>
</button>

根据上述代码,使用findElement(By.cssName())方法去定位元素。

void testSearchButtonEnabled(){
    // get Search button
    searchButton = self.driver.findElement(By.cssName("button"))
    // check Search button is enabled
    assertTrue(searchButton.isEnabled())
}

同样的如果使用findElements(By.cssName())方法去定位元素,将会返回所有的具有相同name属性值的一系列元素。

依据标签名tag name查找

利用标签的方法类似于利用类名等方法进行查找。我们可以轻松的查找出一系列的具有相同标签名的元素。例如我们可以通过查找表中的<tr>来获取行数。

下面有一个HTML的示例,这里在无序列表中使用了<img>标签。

<ul class="promos">
    <li>
        <a href="http://demo.magentocommerce.com/home-decor.html">
            <img src="/media/wysiwyg/homepage-three-column-promo-
        01B.png" alt="Physical &amp; Virtual Gift Cards">
        </a>
    </li>
    <li>
        <a href="http://demo.magentocommerce.com/vip.html">
            <img src="/media/wysiwyg/homepage-three-column-promo-
        02.png" alt="Shop Private Sales - Members Only">
        </a>
    </li>
    <li>
        <a href="http://demo.magentocommerce.com/accessories/
        bags-luggage.html">
            <img src="/media/wysiwyg/homepage-three-columnpromo-
        03.png" alt="Travel Gear for Every Occasion">
        </a>
    </li>
</ul>

这里面我们使用findElements(By.tag())的方式去获取全部的图片,在此之前,我们将会使用findElement(By.tag())去获取到指定的<ul>

具体代码如下:

void testCountOfPromoBannersImages(){
    // get promo banner list
    bannerList = driver.findElement(By.className("promos"))
    // get images from the banner_list
    banners = bannerList.findElements(By.tagName("img"))
    // check there are 20 tags displayed on the page
    assertEqual(20, banners.length)
}

依据链接文字link Text查找

链接文字查找通常比较简单。使用findElement(By.linkText())请查看以下示例

<a href="#header-account" class="skip-link skip-account">
    <span class="icon"></span>
    <span class="label">Account</span>
</a>

测试代码如下:

void testMyAccountLinkIsDisplayed(){
    // get the Account link
    accountLink =
    driver.findElement(By.linkText("ACCOUNT"))
    // check My Account link is displayed/visible in
    // the Home page footer
    self.assertTrue(accountLink.isDisplayed)
}

依据部分链接文字partial text查找

这里依旧使用上述的列子进行代码编写:

void test_account_links(){
    // get the all the links with Account text in it
    accountLinks = self.driver.findElements(By.partialLinkText("ACCOUNT"))
    // check Account and My Account link is
    displayed/visible in the Home page footer
    assertTrue(2, accountLinks.length) 
}

依据XPath进行查找

XPath是一种在XML文档中搜索和定位节点node的一种查询语言。所有的主流Web浏览器都支持XPath。Selenium2可以用强大的XPath在页面中查找元素。

XPath 使用路径表达式来选取 XML 文档中的节点或者节点集。这些路径表达式和我们在常规的电脑文件系统中看到的表达式非常相似。

常用的XPath的方法有starts-with()contains()ends-with()

若想要了解更多关于XPath的内容,请查看http://www.w3school.com.cn/xpath/index.asp

如下有一段HTML代码,其中里面的<img>没有使用ID,name或者类属性,所以我们无法使用之前的方法。亚这里我们可以通过<img>alt属性,定位到指定的tag。

<ul class="promos">
    <li>
        <a href="http://demo.magentocommerce.com/home-decor.html">
            <img src="/media/wysiwyg/homepage-three-column-promo-
        01B.png" alt="Physical &amp; Virtual Gift Cards">
        </a>
    </li>
    <li>
        <a href="http://demo.magentocommerce.com/vip.html">
            <img src="/media/wysiwyg/homepage-three-column-promo-
        02.png" alt="Shop Private Sales - Members Only">
        </a>
    </li>
    <li>
        <a href="http://demo.magentocommerce.com/accessories/
        bags-luggage.html">
            <img src="/media/wysiwyg/homepage-three-columnpromo-
        03.png" alt="Travel Gear for Every Occasion">
        </a>
    </li>
</ul>

具体代码如下:

void testVipPromo(){
    // get vip promo image
    vipPromo = driver.findElement(By.xpath("//img[@alt='Shop Private Sales - Members Only']"))
    // check vip promo logo is displayed on home page
    assertTrue(vipPromo.isDisplayed)
    // click on vip promo images to open the page
    vipPromo.click()
    // check page title
    assertEqual("VIP", driver.title)
}
    

当然,如果使用find_elements_by_xpath()的方法,将会返回所有匹配了XPath查询的元素。

依据CSS选择器进行查找

CSS是一种设计师用来描绘HTML文档的视觉的层叠样式表。一般来说CSS用来定位多种多样的风格,同时可以用来是同样的标签使用同样的风格等。类似于XPath,Selenium2也可以使用CSS选择器来定位元素。

请查看如下的HTML文档。

<div class="minicart-wrapper">
    <p class="block-subtitle">Recently added item(s)
        <a class="close skip-link-close" href="#" title="Close">×</a>
    </p>
    <p class="empty">You have no items in your shopping cart.
    </p>
</div>

我们来创建一个测试,验证这些消息是否正确。

void testShoppingCartStatus(){
    // check content of My Shopping Cart block on Home page
    // get the Shopping cart icon and click to open the
    // Shopping Cart section
    shoppingCartIcon = driver.findElement(By.cssSelector("div.header-minicartspan.icon")
    shoppingCartIcon.click()
    // get the shopping cart status
    shoppingCartStatus = driver.findElement(By.cssSelector("p.empty")).text
    self.assertEqual("You have no items in your shopping cart.",
    shoppingCartStatus)
    // close the shopping cart section
    closeButton = driver.findElement(By.cssSelector("div.minicart-wrappera.close")
    closeButton.click()
}
   

鼠标事件

Web测试中,有关鼠标的操作,不只是单击,有时候还要做右击、双击、拖动等操作。这些操作包含在ActionChains类中。

常用的鼠标方法:

  • contextClick() //右击
  • douchClick() //双击
  • dragAndDrop() //拖拽
  • moveToElement() //鼠标停在一个元素上
  • clickAndHold() // 按下鼠标左键在一个元素上

键盘事件

键盘操作经常处理的如下:

代码 描述
sendKeys(Keys.BACK_SPACE) 删除键(BackSpace)
sendKeys(Keys.SPACE) 空格键(Space)
sendKeys(Keys.TAB) 制表键(Tab)
sendKeys(Keys.ESCAPE) 回退键(Esc)
sendKeys(Keys.ENTER) 回车键(Enter)
sendKeys(Keys.CONTROL,'a') 全选(Ctrl+A)
sendKeys(Keys.CONTROL,'c') 复制(Ctrl+C)

特殊元素定位

iframe 操作

iframe 元素会创建包含另外一个文档的内联框架(即行内框架)。

在一个<html>中,包含了另一个<html>

示例

<html>
  <head>
    <title>iframe示例</title>
  </head>
  <body>
    <h1>
      这里是H1,标记了标题
    </h1>
    <p>
      这里是段落,标记一个段落,属于外层
    </p>
    <div>
      <iframe id="iframe-1">
        <html>
          <body>
            <p>
              这里是个段落,属于内层,内联框架中的
            </p>
            <div id="div-1">
              <p class="hahahp">
                这里是div中的段落,需要被定位
              </p>
            </div>
          </body>
        </html>
      </iframe>
    </div>
  </body>
</html>

需要定位上面示例中的<p>:这里是div中的段落,需要被定位

如下是selenium WebDiriver的代码

Java代码

// 这里涉及了iframe操作
// 1. 找到 iframe 元素对应的 css selector,存到变量中
// 2. 调用driver.switchTo().frame(刚刚被找到的iframe元素)
WebElement frameElement = driver.findElement(By.cssSelector("#iframe-1"));
driver.switchTo().frame(frameElement);
// 进入紫禁城以后,正常操作
driver.findElement(By.cssSelector("XXX")).click();
Thread.sleep(2000);

// TODO:继续填写表单

// 在紫荆城结束以后必须退出来
// switchTo().frame()和 switchTo().defaultContent() 成对出现
driver.switchTo().defaultContent();

Python代码

## 查找并定位 iframe
element_frame = driver.find_element_by_css_selector('#iframe-1')
## 切换到刚刚查找到的 iframe
driver.switch_to.frame(element_frame)
## 定位 <p>
driver.find_element_by_css_selector('#div-1 > p')
## TODO....
## 退出刚刚切换进去的 iframe
driver.switch_to.default_content()

Select 操作

<select> 是选择列表

Select 是个selenium的类selenium.webdriver.support.select.Select

Select 类的路径:

C:\Python35\Lib\site-packages\selenium\webdriver\support\select.py

<select id="brand">
  <option value ="volvo">Volvo</option>
  <option value ="saab">Saab</option>
  <option value="opel">Opel</option>
  <option value="audi">Audi</option>
</select>

示例,选择 Audi

Java代码

// 遇到 <select>
// 1. 定位 select,保存这个select 元素到变量中
// 2. 对变量进行操作,把变量作为一个参数,用new Select(变量)产生一个可操作的对象
// 3. 调用对象的方法
WebElement elementSelect = driver.findElement(By.cssSelector("#type"));
Select objectSelect = new Select(elementSelect);
objectSelect.selectByIndex(2);

Python代码

## 查找并定位到 select
element_select = driver.find_element_by_css_selector('#brand')
## 用Select类的构造方法,实例化一个对象 object_select
object_select = Select(element_select)
## 操作 object_select
object_select.select_by_index(3)
## 也可以这样
object_select.select_by_value('audi')
## 还可以这样
object_select.select_by_visible_text('Audi')

高级用户交互API

高级用户交互API提供了一个更新更完善的机制来定义并描述用户在一个网页上的各种操作。这些操作包括:拖拽、按住CTRL键选择多个元素等等。

快速上手

为了生成一连串的动作,我们使用Actions来建立。首先,我们先配置操作:

Actions builder = new Actions(driver);

builder.keyDown(Keys.CONTROL)
.click(someElement)
.click(someOtherElement)
.keyUp(Keys.CONTROL);

然后,获得操作(Action):

Action selectMultiple = builder.build();

最后,执行这个动作:

selectMultiple.perform();   

这一系列的动作应该尽量的短。在使用中最好在执行一个简短的动作后验证页面是否处于正确的状态,然后再执行下面的动作。下一节将会列出所有可用的动作(Action),并且说明它们如何进行扩展。

键盘交互(Keyboard interactions)

键盘交互是发生在一个特定的页面元素的,而webdriver会确保这个页面元素在执行键盘动作时处于正确的状态。这个正确的状态,包括页面元素滚动到可视区域并定位到这个页面元素。
既然这个新的API是面向用户(user-oriental)的接口,那么对于一个用户,在对一个元素输入文本前做显式的交互就更加的符合逻辑。这意味着,当想定位到相邻的页面元素时,可能需要点击一下元素或按下Tab(Keys.TAB)键。
The new interactions API will (first) support keyboard actions without a provided element. The additional work to focus on an element before sending it keyboard events will be added later on.

鼠标交互(Mouse interactions)

鼠标操作有一个上下文-鼠标的当前位置。因此,当为几个鼠标操作设定一个上下文时,第一个操作的上下文就是元素的相对位置,下一个操作的上下文就上一个操作后的鼠标相对位置。

单个动作

所有的动作都实现了Action接口,这个接口只有一个方法:perform()。每个动作所需要的信息都通过Constructor传入。当调用这个动作的时候,动作知道如何与页面交互(如,找到活动的元素并输入文本或者计算出在屏幕上的点击坐标)并且调用底层实现来实现这个交互。
下面是一些动作:

  • ButtonReleaseAction - 释放鼠标左键
  • ClickAction - 相当于 WebElement.click()
  • ClickAndHoldAction - 按下鼠标左键并保持
  • ContextClickAction - 一般就是鼠标右键,通常调出右键菜单用。
  • DoubleClickAction - 双击某个元素
  • KeyDownAction - 按下修饰键(SHIFT,CTRL,ALT,WIN)
  • KeyUpAction - 释放修饰键
  • MoveMouseAction - 移动鼠标从当前位置到另外的元素.
  • MoveToOffsetAction - 移动鼠标到一个元素的偏移位置(偏移可以为负,元素是鼠标刚移动到的那个元素)。
  • SendKeysAction - 相当于 WebElement.sendKey(...)

CompositeAction包含一系列的动作,当被调用的时候,它会调用它所包含的所有动作的perform方法。通常,这个动作通常都不是直接建立的,一般是使用ActionChainsGenerator

生成动作链

Actions链生成器实现了创建者模式来新建一个包含一组动作的CompositeAction。使用Actions生成器可以很容易的生成动作并调用build()方法来获得复杂的操作。

Actions builder = new Actions(driver);

Action dragAndDrop = builder.clickAndHold(someElement)
   .moveToElement(otherElement)
   .release(otherElement)
   .build();

dragAndDrop.perform();

有一个对Actions进行扩展的计划,给Actions类添加一个方法,这个方法可以追加任何动作到其拥有的动作列表上。这将允许添加扩展的动作,而不用人工创建CompositeAction。关于扩展Actions,请往下看。

扩展Action接口的指导

Action接口只有一个方法-perform()。除了实际的交互本身,所有的条件判断也都应该在这个这个方法里实现。在动作创建和动作实际执行这段时间内,很可能页面的状态已经发生了变化,比如元素的可视情况已经坐标已经不能找到了。

实现细节

为了达到每个操作的执行与具体实现的分离,所有的动作都依赖2个接口:MouseKeyboard。这些接口被所有支持高级用户接口API的driver实现了。需要注意的是,这些接口是为了让动作使用的,而不是最终用户。本节的信息,主要是针对想扩展WebDriver的开发者的。

一字警告

KeyboardMouse接口是设计用来支持各种Action类的。有鉴于此,它们的API没有Actions链生成器API稳定。直接使用这些接口可能达不到期望的结果,因为Action类做了额外的工作来确保在实际事件触发时处于正确的环境条件。这些准备工作包括定位在正确的元素上或者鼠标交互前保证元素是可见的。

Keyboard接口

Keyboard接口包含3个方法:

  • void sendKeys(CharSequence... keysToSend) - 与 sendKeys(...)相似.
  • void pressKey(Keys keyToPress) - 按下一个键并保持。键仅限于修饰键(Control, Alt and Shift).
  • void releaseKey(Keys keyToRelease) - 释放修饰键.

至于如何在调用之间保存修饰键的状态是Keyboard接口实现类的职责。只有活跃的元素才会接收到这些事件。

Mouse接口

Mouse接口包含以下方法(有可能不久之后会有变化):

  • void click(WebElement onElement) - 同click()方法一样.
  • void doubleClick(WebElement onElement) - 双击一个元素.
  • void mouseDown(WebElement onElement) - 在一个元素上按下左键并保持 Action selectMultiple = builder.build();
  • void mouseUp(WebElement onElement) - 在一个元素上释放左键.
  • void mouseMove(WebElement toElement) - 从当前位置移动到一个元素
  • void mouseMove(WebElement toElement, long xOffset, long yOffset) - 从当前位置移动到一个元素的偏移坐标
  • void contextClick(WebElement onElement) - 在一个元素上做一个右键操作

Native events(原生事件) VS synthetic events(合成事件)

WebDriver提供的高级用户接口,要么是直接模拟的Javascript事件(即合成事件),要么就是让浏览器生成Javascript事件(即原生事件)。原生事件能更好的模拟用户交互,而合成事件则是平台独立的,这使得使用了替代的窗口管理器的linux系统显得尤其重要,具体参加native events on Linux。原生事件无论什么时候总是应该尽可能的使用。

下面的表格展示了浏览器对事件的支持情况。

浏览器 操作系统 原生事件 合成事件
Firefox Linux 支持 支持(默认)
Firefox Windows 支持(默认) 支持
Internet Explorer Windows 支持(默认) 不支持
Chrome Linux/Windows 支持* 不支持
Opera Linux/Windows 支持(默认) 不支持
HtmlUnit Linux/Windows 支持(默认) 不支持

ChromeDriver提供了2种模式来支持原生事件:Webkit事件和原始事件。其中Webkit事件是使用Webkit函数来触发的Javascript事件,而原始事件模式则使用的是操作系统级别的事件。

FirefoxDriver中,原生事件可以使用FirefoxProfile来进行开关控制。

FirefoxProfile profile = new FirefoxProfile();
profile.setEnableNativeEvents(true);
FirefoxDriver driver = new FirefoxDriver(profile);

例子

以下是原生事件与合成事件表现不同的一些例子:

  • 使用合成事件,点击隐藏在其他元素下面的元素是可以的。使用原生事件,浏览器会将点击事件作用在所给坐标最外层的元素上,就像是用户点击在特定的位置一样。
  • 当一个用户,按下TAB键希望焦点从当前元素定位到下一个元素,浏览器是可以做到的。使用合成事件的话,浏览器并不知道TAB键被按下了,因此也不会改变焦点。而使用原生事件,浏览器则会表现正确。

模块化与类库

线性测试

至此之前,我们介绍的测试脚本,尽管使用了unittest测试框架,但是测试是按照指定的线路进行的,是线性的测试,完全遵循了脚本的执行顺序。

测试脚本1

driver = FirefoxDriver()
driver.get("http://wwww.xxx.com")
driver.findElement(By.id("tbUserName")).sendKeys("username")
driver.findElement(By.id(("tbPassword")).sendKeys("123456")
driver.findElement(By.id(("btnLogin")).click()
#执行具体用例操作
......
driver.quit ()

如上图,其实登录的模块可以共用。

模块化

  1. 使用模块化的业务组装测试
    • 业务从测试用例抽离
    • 具体步骤:
      1. 用IDEA新建maven项目

      2. 修改pom.xml未指定的内容

      3. 右上角(或者右下角)点击 Enable Auto-import

      4. src/main/java 新建Java Class(测试用例)

      5. 新建的测试用例中写三个方法:(需要testNg的注解)

        • public void setUp(@BeforeTest)
        • public void tearDown(@AfterTest)
        • public void testXxx(@Test)
      6. src/main/java 新建Java Class2(测试业务)

      7. 在新建的测试业务类中写业务的方法:

        • openUrl
        • changeLanguage
        • logIn
      8. 在新建的测试业务类中写构造方法

        注意需要传递driverurl

        public RanzhiCommon(WebDriver driver, String url){
          this.baseDriver = driver;
          this.baseUrl = url;
        }
        

模块话是自动化测试的第一个延伸和基础。需要对自动化重复编写的脚本进行重构(refactor),将重复的脚本抽取出来,放到指定的代码文件中,作为共用的功能模块。

测试脚本1:RanzhiCommon.java

/**
* 实际上的登录方法
*
* @param account:用户名
* @param password:密码
*/
public void logIn(String account, String password) {
    WebDriver driver = this.driver;
    // 输入用户名
    WebElement accountElement = driver.findElement(By.id("account"));
    accountElement.clear();
    accountElement.sendKeys(account);

    // 输入密码
    WebElement passwordElement = driver.findElement(By.id("password"));
    passwordElement.clear();
    passwordElement.sendKeys(password);

    // 点击登录按钮
    driver.findElement(By.id("submit")).click();

    try {
        Thread.sleep(2000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
}

另一份文件 RanzhiCommon.java

/**
* 登出系统
*/
public void logOut() {
    WebDriver driver = this.driver;
    driver.findElement(By.id("start")).click();
    driver.findElement(By.linkText("退出")).click();
    try {
        Thread.sleep(1000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }

}

自动化的测试:代码如下

package com.hello;

import com.hello.library.RanzhiCommon;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.firefox.FirefoxDriver;

/**
 * Created by Linty on 9/11/2016.
 * 使用模块化的测试用例
 */
public class RanzhiTestCase02 {

    // 成员变量
    private WebDriver driver;
    private String baseUrl;
    private RanzhiCommon common;

    @Before
    public void setUp() {
        this.driver = new FirefoxDriver();
        this.baseUrl = "http://172.31.95.220/ranzhi/www";
        this.common = new RanzhiCommon(this.driver, this.baseUrl);
    }

    @After
    public void tearDown() {

        this.driver.quit();
        this.common = null;
    }

    @Test
    public void testLoginWithModule() {
        WebDriver driver = this.driver;
        RanzhiCommon common = this.common;
        // 步骤一:打开页面
        common.openWebPage("/");
        Assert.assertEquals("登录页面打开错误",
                this.baseUrl + "/sys/index.php?m=user&f=login&referer=L3JhbnpoaS93d3cvc3lzL2luZGV4LnBocA==",
                driver.getCurrentUrl());

        // 步骤二:切换语言
        String actualLanguage = common.changeChinese();

        Assert.assertEquals("系统语言切换失败", "简体", actualLanguage);

        // 步骤三:进行登录
        common.logIn("admin", "123456");
        Assert.assertEquals("登录页面登录跳转失败",
                this.baseUrl + "/sys/index.php?m=index&f=index",
                driver.getCurrentUrl());

        // 步骤四:退出系统
        common.logOut();
        Assert.assertEquals("登录页面退出跳转失败",
                this.baseUrl + "/sys/index.php?m=user&f=login",
                driver.getCurrentUrl());
    }
}

参数化驱动

数据驱动

如果说模块化是自动化测试的第一步,那么数据驱动是自动化的第二步,从本意上来讲。数据改变更新驱动自动化的执行。从而引起测试结果的改变。其实类似于参数化。

  • 使用数据驱动测试

    • 测试数据从用例抽离
    • 常见的测试数据的形式:
    1. 外部文件(文本文件、Excel(带有格式,不容易读))
      • csv(默认是用","隔开每一列)
      • txt
    2. 数据库的方式
      • MySQL (轻便)
      • Oracle/ SQL Server
  • 具体进行数据驱动的方式:

    1. 找一个类,或者编写一个类
      • 找读取csv的类:maven的方式

示例代码

@Test
public void testAddBatchUserByCsv() {

  // 读取 csv 文件到FilerReader中
  // 用捕获异常的方式 进行文件读取,防止出现“文件不存在”的异常
  Reader in = null;
  try {
    in = new FileReader("src/main/resources/team_member.csv");
  } catch (FileNotFoundException e) {
    e.printStackTrace();
  }

  // 读取 csv 到 records中
  Iterable<CSVRecord> records = null;
  try {
    records = CSVFormat.EXCEL.parse(in);
  } catch (IOException e) {
    e.printStackTrace();
  }
  // 遍历 records,循环添加 userToAdd
  for (CSVRecord record : records) {
    RanzhiUser userToAdd = new RanzhiUser(
      record.get(0), record.get(1),
      Integer.parseInt(record.get(3)),
      Integer.parseInt(record.get(4)),
      record.get(2).toCharArray()[0],
      record.get(5),
      record.get(0) + record.get(6)
    );

    baseRanzhiCommon.login("admin", "123456");

    baseDriver.manage().timeouts().implicitlyWait(10, TimeUnit.SECONDS);
    String expectedMainUrl = baseUrl + "sys/index.php?m=index&f=index";
    Assert.assertEquals("登录成功主页跳转失败", expectedMainUrl, baseDriver.getCurrentUrl());

    baseRanzhiCommon.selectApp(RanzhiApp.Admin);
    baseDriver.manage().timeouts().implicitlyWait(10, TimeUnit.SECONDS);
    String expectedAdminUrl = baseUrl + "sys/index.php?m=admin&f=index";
    Assert.assertEquals("后台管理主页跳转失败", expectedAdminUrl, baseDriver.getCurrentUrl());

    baseDriver.switchTo().frame("iframe-superadmin");
    baseDriver.manage().timeouts().implicitlyWait(10, TimeUnit.SECONDS);

    baseRanzhiCommon.selectSubMenuForAdmin(AdminSubMenu.Organization);


    String expectedOrganizationUrl = baseUrl + "sys/index.php?m=admin&f=index";
    Assert.assertEquals("后台管理组织跳转失败", expectedOrganizationUrl, baseDriver.getCurrentUrl());

    baseRanzhiCommon.clickAddUserButton();
    baseDriver.manage().timeouts().implicitlyWait(10, TimeUnit.SECONDS);

    String expectedAddUserUrl = baseUrl + "sys/index.php?m=user&f=create";
    Assert.assertEquals("添加成员主页跳转失败", expectedAddUserUrl, baseDriver.getCurrentUrl());

    baseRanzhiCommon.addNewUser(userToAdd);
    baseDriver.manage().timeouts().implicitlyWait(10, TimeUnit.SECONDS);
    try {
      sleep(5000);
    } catch (InterruptedException ignored) {
    }
    String expectedUserSavedUrl = baseUrl + "sys/index.php?m=user&f=admin";
    Assert.assertEquals("用户保存跳转失败", expectedUserSavedUrl, baseDriver.getCurrentUrl());
    baseDriver.switchTo().defaultContent();

    baseRanzhiCommon.logout();
    baseDriver.manage().timeouts().implicitlyWait(10, TimeUnit.SECONDS);

    String expectedLogoutUrl = baseUrl + "sys/index.php?m=user&f=login";
    Assert.assertEquals("退出登录页面跳转错误", expectedLogoutUrl, baseDriver.getCurrentUrl());

    baseRanzhiCommon.login(userToAdd.getAccount(), userToAdd.getPassword());

    baseDriver.manage().timeouts().implicitlyWait(10, TimeUnit.SECONDS);
    String expectedMainUrl2 = baseUrl + "sys/index.php?m=index&f=index";
    Assert.assertEquals("登录成功主页跳转失败", expectedMainUrl2, baseDriver.getCurrentUrl());

    baseRanzhiCommon.logout();
    baseDriver.manage().timeouts().implicitlyWait(10, TimeUnit.SECONDS);

    String expectedLogoutUrl2 = baseUrl + "sys/index.php?m=user&f=login";
    Assert.assertEquals("退出登录页面跳转错误", expectedLogoutUrl2, baseDriver.getCurrentUrl());

  }


}

关于参数化驱动,我们可以将数据放到csv中,然后通过读取csv的数据进行自动化测试。同时也可以使用数据库尝试,代码如下:

@Test
public void testAddBatchUserByDb() {
  Statement stmt = null;
  ResultSet rs = null;
  try {
    Class.forName("com.mysql.cj.jdbc.Driver").newInstance();
  } catch (InstantiationException e) {
    e.printStackTrace();
  } catch (IllegalAccessException e) {
    e.printStackTrace();
  } catch (ClassNotFoundException e) {
    e.printStackTrace();
  }
  try {
    Connection conn = null;
    try {
      conn = DriverManager.getConnection("jdbc:mysql://localhost/test?" +
                                         "user=root&password=");
    } catch (SQLException e) {
      e.printStackTrace();
    }

    stmt = conn.createStatement();
    String sql = "SELECT \n" +
      "  `account`,\n" +
      "  `realname`,\n" +
      "  `gender`,\n" +
      "  `dept`,\n" +
      "  `role`,\n" +
      "  `password`,\n" +
      "  `email` \n" +
      "FROM\n" +
      "  `test`.`userlist` \n" +
      "LIMIT 0, 1000 ;";
    rs = stmt.executeQuery(sql);

    // or alternatively, if you don't know ahead of time that
    // the query will be a SELECT...

    if (stmt.execute(sql)) {
      rs = stmt.getResultSet();
      System.out.println(rs.next());
    }
    if (rs != null) {
      while (rs.next()) {
        RanzhiUser userToAdd = new RanzhiUser(
          rs.getString("account"),
          rs.getString("realname"),
          Integer.parseInt(rs.getString("role")),
          Integer.parseInt(rs.getString("dept")),
          rs.getString("gender").toCharArray()[0],
          rs.getString("password"),
          rs.getString("account") + rs.getString("email")
        );

        baseRanzhiCommon.login("admin", "123456");

        baseDriver.manage().timeouts().implicitlyWait(10, TimeUnit.SECONDS);
        String expectedMainUrl = baseUrl + "sys/index.php?m=index&f=index";
        Assert.assertEquals("登录成功主页跳转失败", expectedMainUrl, baseDriver.getCurrentUrl());

        baseRanzhiCommon.selectApp(RanzhiApp.Admin);
        baseDriver.manage().timeouts().implicitlyWait(10, TimeUnit.SECONDS);
        String expectedAdminUrl = baseUrl + "sys/index.php?m=admin&f=index";
        Assert.assertEquals("后台管理主页跳转失败", expectedAdminUrl, baseDriver.getCurrentUrl());

        baseDriver.switchTo().frame("iframe-superadmin");
        baseDriver.manage().timeouts().implicitlyWait(10, TimeUnit.SECONDS);

        baseRanzhiCommon.selectSubMenuForAdmin(AdminSubMenu.Organization);


        String expectedOrganizationUrl = baseUrl + "sys/index.php?m=admin&f=index";
        Assert.assertEquals("后台管理组织跳转失败", expectedOrganizationUrl, baseDriver.getCurrentUrl());

        baseRanzhiCommon.clickAddUserButton();
        baseDriver.manage().timeouts().implicitlyWait(10, TimeUnit.SECONDS);

        String expectedAddUserUrl = baseUrl + "sys/index.php?m=user&f=create";
        Assert.assertEquals("添加成员主页跳转失败", expectedAddUserUrl, baseDriver.getCurrentUrl());

        baseRanzhiCommon.addNewUser(userToAdd);
        baseDriver.manage().timeouts().implicitlyWait(10, TimeUnit.SECONDS);
        try {
          sleep(5000);
        } catch (InterruptedException ignored) {
        }
        String expectedUserSavedUrl = baseUrl + "sys/index.php?m=user&f=admin";
        Assert.assertEquals("用户保存跳转失败", expectedUserSavedUrl, baseDriver.getCurrentUrl());
        baseDriver.switchTo().defaultContent();

        baseRanzhiCommon.logout();
        baseDriver.manage().timeouts().implicitlyWait(10, TimeUnit.SECONDS);

        String expectedLogoutUrl = baseUrl + "sys/index.php?m=user&f=login";
        Assert.assertEquals("退出登录页面跳转错误", expectedLogoutUrl, baseDriver.getCurrentUrl());

        baseRanzhiCommon.login(userToAdd.getAccount(), userToAdd.getPassword());

        baseDriver.manage().timeouts().implicitlyWait(10, TimeUnit.SECONDS);
        String expectedMainUrl2 = baseUrl + "sys/index.php?m=index&f=index";
        Assert.assertEquals("登录成功主页跳转失败", expectedMainUrl2, baseDriver.getCurrentUrl());

        baseRanzhiCommon.logout();
        baseDriver.manage().timeouts().implicitlyWait(10, TimeUnit.SECONDS);

        String expectedLogoutUrl2 = baseUrl + "sys/index.php?m=user&f=login";
        Assert.assertEquals("退出登录页面跳转错误", expectedLogoutUrl2, baseDriver.getCurrentUrl());

      }
    }
    // Now do something with the ResultSet ....
  } catch (SQLException ex) {
    // handle any errors
    System.out.println("SQLException: " + ex.getMessage());
    System.out.println("SQLState: " + ex.getSQLState());
    System.out.println("VendorError: " + ex.getErrorCode());
  } finally {
    // it is a good idea to release
    // resources in a finally{} block
    // in reverse-order of their creation
    // if they are no-longer needed

    if (rs != null) {
      try {
        rs.close();
      } catch (SQLException sqlEx) {
      } // ignore

      rs = null;
    }

    if (stmt != null) {
      try {
        stmt.close();
      } catch (SQLException sqlEx) {
      } // ignore

      stmt = null;
    }
  }
}

Git的使用

  1. git
    git vs svn
  • 配置管理工具

  • 手工测试:文档(测试计划、用例、测试报告、需求、用户手册)

  • 开发:源代码管理

  • 安装Git的步骤
    教室内网安装包:
    \\172.31.95.220\share\01-新教学软件\01-自动化测试\git安装

    1. 安装git程序
      git官网:https://git-scm.com
      git本身是命令行工具
      官网推荐git桌面工具:SourceTree
    2. 安装TortoiseGit客户端
  • 选择Git仓库

    1. 最有名的 https://github.com,开源代码的主战场
    2. 国内的
      • coding.net
      • 码云:git.oschina.net
      • code.csdn.net
    3. 选择 coding.net
  • 具体使用步骤:

    1. 在coding.net 初始化Git仓库(先注册用户,登录)
    2. 创建项目(启用README.md初始化项目)
      • md文件:Markdown文件
    3. 在coding.net的项目左侧,找到代码,复制旁边的路径
    4. 在非系统盘创建文件夹 git
    5. 进入git,右键选择 git clone
    6. 弹出的对话框中,默认就有刚刚复制的路径,检查文件存放的位置
      • 比如 git\selenium_weekend
      • 确定,输入用户名 + 密码
    7. 初始化成功
  • 修改文件并提交(用TortoiseGit操作)

    1. 从Git仓库更新当前的文件
      • TortoiseGit右键,选择Git Sync
      • 弹出的对话框中,选择pull:拉取文件
      • 如果是私有Git仓库,需要输入用户名 + 密码
    2. 编辑需要的文件
    3. 提交所处理的变动
      • TortoiseGit右键,选择Git Commit
      • 选择push:提交文件
      • 如果是私有Git仓库,需要输入用户名 + 密码
  • 使用编程工具提交代码(用IDEA或者PyCharm)

    1. 在本地的Git项目文件夹中创建项目
      • 比如:git\selenium_weekend
      • 用IDEA 创建 Maven项目
        • 注意 project location务必在 git\selenium_weekend
        • 比如 项目名字 HelloSelenium
        • 项目路径:git\selenium_weekend\HelloSelenium
    2. 覆盖 pom.xml
    3. 编写 代码
    4. 选择 IDEA的 VCS | Enable Version Control Integration
    5. 弹出的窗口选择 Git
      • 所有的代码文件名字变成红色
    6. 右键 左侧的项目名字 HelloSelenium
      • 选择 Git | Add
      • 所有的代码文件名字变成绿色
    7. 右键 左侧的项目名字 HelloSelenium
      • 选择 Git | Commit Directory
      • 左侧填写 说明
      • 右侧勾选 Reformat Code
      • 选择右下角 Commit And Push
      • "XX文件已经committed"以后,点击 Push
      • 输入用户名 + 密码,勾选 remember password
      • push successful

阶段小结

这里的数据换成了特别的数据,就是关键字。

  1. Selenium IDE 的作用
    通过录制页面元素的定位和操作,进行自定义命令的编辑,(Select定位页面元素),导出指定的带单元测试框架的脚本(自动化测试用例),辅助代码编写,以及快速入门。

  2. Selenium Java 环境搭建

      使用maven的方式,管理依赖项,进行环境搭建
      新建maven的项目,编辑项目中的 pom.xml,*.jar jar包
    
    • 依赖项要添加对,包名和版本
    • 电脑联网,需要连接访问maven仓库
  3. Selenium WebDriver 的元素定位
    需要注意iFrame

    • 单一元素定位

      id, name, class name(不靠谱), tag name(不靠谱)
      xpath, css selector, link text, partial link text

      • class是以开头

      • id 是以# 开头

      • css selector:#langs > button

      • xpath://*[@id="langs"]/button

      • xpath(绝对路径):/html/body/div/div/div[1]/div/div/button

        解释

        <html>
            <body>
                <div>
                    <div>
                        <div>
                            <div>
                                <div>
                                    <button></button>
                                </div>
                            </div>
                        </div>
                        <div></div>
                    </div>
                <div>
            </body>
        </html>
        

    • 定位一系列元素

      id name (不靠谱)
      class name
      css selector

      xpath

  4. Selenium 使用 单元测试框架

      Java: junit4/TestNG
      Python: unittest
    
    项目 Java Python
    方法名 小骆驼 testRanzhiLogin() 类C的命名test_ranzhi_login()
    用例的步骤 @Test(方法以test开头) test_开头
    用例的前置条件 @Before(方法 setUp) 重写setUp()
    用例的清场方式 @After(方法 tearDown) 重写tearDown()
    依赖的处理 添加junit依赖项到pom.xml import unittest 写一个测试用例类,继承unittest.TestCase
  5. Selenium 使用 模块化 用例设计

    1. library文件夹
      RanzhiCommon.java

    2. 面向对象:实例化一个对象(注意构造方法),调用对象的成员,包括普通方法,变量

      Java实体类
      Java枚举类型

      设计模式:自动化测试框架,页面对象模式

      python 面向对象

      dict类型

  6. Selenium 使用 数据驱动 进行用例设计和执行

    1. 读文件进行测试(准备的用例数据)
    2. 读数据库进行测试(不推荐)
      • 代码中,注意关闭数据库连接
      • 代码中,注意SQL脚本的效率,
      • 替代方案,导出数据库的数据到文件中,然后读文件,或者把该文件导入本地的数据库,注意类型转换,尤其是日期类型。
    3. 新建一个CsvUtility.java,新建一个DbUtility.java
  7. Selenium 工具的使用汇总 IDEA 和 Git

      `IDEA` `PyCharm`
      打开对的文件夹(作为项目)
      Reformat Code (Ctrl + Alt + L)   
      Refactor | rename 重构,修改名字,批量关联的级联修改
      按下ctrl+鼠标左键,导航定位的声明处
      红灯处理,选中错误的行,然后等红灯出来,点右上角的箭头
      Java里面 用 Alt + Enter 键
      Git的集成:
      
        github.io
        coding.net
        git.oschina.net
        bitbucket.org
        code.csdn.net(腾讯的测试工具 apt)
    
    • 在线建仓库
      • git clone 下载到本地(需要新建非系统盘的 git文件夹)
      • git pull 获取远程变化到本地
      • commit 提交到本地
      • push 提交到远程
        集成在IDEA中
        菜单 VCS | Enable ……,里面选择git
        提交git 需要先add
        导出一份 完全没有版本管理信息的代码。 export
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342

推荐阅读更多精彩内容