Selenium:玩耍Web自动化

与服务端的测试不同,Web前端的测试基本都是依赖于浏览器,试想一下,正常用户行为就是基于浏览器来做各种页面上的增删改查,因此对于测试而言,就需要覆盖全用户能够执行的所有操作,除此之外用户的个人喜好不同,浏览器的选择以及版本的选择都五花八门,各种控件组件兼容性都容易出现异常,可见每次要测试的内容丰富而且麻烦,特别是如果还需要重复回归旧的功能,多来几次估计就十分废鼠标,对标一下服务端的自动化测试来考虑,如果要实现Web前端的自动化测试,应该是这样一种流程,我们写好测试代码,调用不同类型浏览器接口的API,来和浏览器交互,达到模仿用户行为的操作

有了上面的想法,可以来看一套基于WEB的自动化测试工具,Selenium的实现,它提供了一个WebDriver,既要监听测试脚本请求的测试动作,又要根据不同驱动类型去请求特定的浏览器API来完成具体的操作动作,简单画了张图,大致流程如下:

1、创建一个WebDriver实例,这时候会启动一个web服务

2、执行前端页面操作请求,web服务监听到了请求后,根据WebDriver实例的类型,调用具体类型浏览器的API来完成操作

3、浏览器完成具体页面响应操作

NewImage

具体来做下测试

1、安装WebDriver,我是本地测试脚本执行本地的WebDriver,直接下载ChromeDriver,可以根据chrome版本选一个相近的安装,下载链接:https://chromedriver.storage.googleapis.com/index.html

下载好了之后,直接移到$PATH当中去,mv ./chromedriver /usr/local/bin,不然就需要测试代码里指定配置路径

2、可以验证一下ChromeDriver的功能,启动

 ✘ lihui@2022  ~  chromedriver
Starting ChromeDriver 99.0.4844.51 (d537ec02474b5afe23684e7963d538896c63ac77-refs/branch-heads/4844@{#875}) on port 9515
Only local connections are allowed.
Please see https://chromedriver.chromium.org/security-considerations for suggestions on keeping ChromeDriver safe.
[1647529567.525][WARNING]: FromSockAddr failed on netmask
ChromeDriver was started successfully.

可以看到监听了端口9515,直接curl下

 lihui@2022  ~  curl -s 127.0.0.1:9515 | jq .
{
  "value": {
    "error": "unknown command",
    "message": "unknown command: unknown command: ",
    "stacktrace": "0   chromedriver                        0x00000001098c2159 chromedriver + 5120345\n1   chromedriver                        0x000000010984fb13 chromedriver + 4651795\n2   chromedriver                        0x000000010943fe68 chromedriver + 392808\n3   chromedriver                        0x00000001094903ae chromedriver + 721838\n4   chromedriver                        0x0000000109490127 chromedriver + 721191\n5   chromedriver                        0x00000001094187da chromedriver + 231386\n6   chromedriver                        0x000000010987f38d chromedriver + 4846477\n7   chromedriver                        0x000000010989921c chromedriver + 4952604\n8   chromedriver                        0x000000010989ea12 chromedriver + 4975122\n9   chromedriver                        0x0000000109899b4a chromedriver + 4954954\n10  chromedriver                        0x00000001098745b0 chromedriver + 4801968\n11  chromedriver                        0x00000001094178ae chromedriver + 227502\n12  libdyld.dylib                       0x00007fff6928ccc9 start + 1\n"
  }
}

3、可以写测试代码了,这里有一个超级详细的中文文档可以参考,python的:https://selenium-python-zh.readthedocs.io/en/latest/

我这里用java,首先加一个maven依赖,已经集成在springboot里了,只需要依赖一下就行了

<dependency>
    <groupId>org.seleniumhq.selenium</groupId>
    <artifactId>selenium-java</artifactId>
</dependency>

来一个google搜索lihuia.com的操作

import org.openqa.selenium.By;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.chrome.ChromeDriver;

import java.util.concurrent.TimeUnit;

public class GoogleSearch {

public static void main(String[] args) throws InterruptedException {
// 1.创建webdriver驱动
WebDriver driver = new ChromeDriver();
// 2.打开google
driver.get("https://google.com");
// 3.获取输入框,输入lihuia.com
driver.findElement(By.name("q")).sendKeys("lihuia.com");
// 4.获取"Google搜索"按钮,进行搜索
driver.findElement(By.name("btnK")).submit();
TimeUnit.SECONDS.sleep(5);
// 5.退出浏览器
driver.quit();
}
}

大概意思根据注释也能很容易看懂,但是有一点,findElement里面的条件是如何获取的,有种笨办法,检查元素

NewImage

下面的Google搜索按钮同理即可;这里我只是举个例子,其实这里不严谨,假如有多个元素name为q,name会定位到第一个,就不一定是这个输入框了

所以测试不同的浏览器,首先得新建不同类型的WebDriver,可以看到目前实现了8种类型

UntitledImage

 

WebDriver里提供的和浏览器交互的API在包package org.openqa.selenium里的WebDriver接口中

方法 描述
get(String url) 访问目标 url 地址,打开网页
getCurrentUrl() 获取当前页面 url 地址
getTitle() 获取页面标题
getPageSource() 获取页面源代码
close() 关闭浏览器当前打开的窗口
quit() 关闭浏览器所有的窗口
findElement(by) 查找单个元素
findElements(by) 查到元素列表,返回一个集合
getWindowHandle() 获取当前窗口句柄
getWindowHandles() 获取所有窗口的句柄

 

和浏览器上元素控件交互的API在包package org.openqa.selenium的WebElement接口里

方法 描述
click() 对元素进行点击
clear() 清空内容(如文本框内容)
sendKeys(…) 写入内容与模拟按键操作
isDisplayed() 元素是否可见(true:可见,false:不可见)
isEnabled() 元素是否启用
isSelected() 元素是否已选择
getTagName() 获取元素标签名
getAttribute(attributeName) 获取元素对应的属性值
getText() 获取元素文本值(元素可见状态下才能获取到)
submit() 表单提交

 

最后,和浏览器交互之后,也知道了控件元素如何操作,可如何定位到这个元素呢,因为网页最终是一对HTML文件,因此WebDriver又提供了8中定位策略,找到对应的元素,在包package org.openqa.selenium的By类

方法 描述 参数 示例
findElement(By.id()) 通过元素的 id 属性值来定位元素 对应的id属性值 findElement(By.id(“kw”))
findElement(By.name()) 通过元素的 name 属性值来定位元素 对应的name值 findElement(By.name(“user”))
findElement(By.className()) 通过元素的 class 名来定位元素 对应的class类名 findElement(By.className(“passworld”))
findElement(By.tagName()) 通过元素的 tag 标签名来定位元素 对应的标签名 findElement(By.tagName(“input”))
findElement(By.linkText()) 通过元素标签对之间的文本信息来定位元素 文本内容 findElement(By.linkText(“登录”))
findElement(By.partialLinkText()) 通过元素标签对之间的部分文本信息来定位元素 部分文本内容 findElement(By.partialLinkText(“百度”))
findElement(By.xpath()) 通过xpath语法来定位元素 xpath表达式 findElement(By.xpath(“//input[@id=’kw’]”))
findElement(By.cssSelector()) 通过css选择器来定位元素 css元素选择器 findElement(By.cssSelector(“#kw”))

 

因此,任何一段操作,都可以分解为操作浏览器,定位到特定元素,浏览器上操作这些元素这几步,由此上面那段测试代码就会打开chrome浏览器,输入google.com之后,搜索lihuia.com这个操作

 

虽然上面是正向思维完成了一个从测试脚本,到完成测试的过程,但显然这样写起测试代码十分痛苦,因为定位到页面每个空间,元素十分麻烦,因此是否可以直接在浏览器上先点击一遍,然后录制下操作动作,可以自定义修改参数,最后还能导出成测试代码呢,这个是有的,很完美地发现了Selenium IDE

Selenium ID可以模拟浏览器行为,录制操作动作,回放和导出测试脚本,因此可以帮我们做一些重复的人工测试;由于它是依托于浏览器,因此最终支持各种浏览器的使用,都是基于浏览器插件

首先浏览器安装插件,Chrome应用市场搜索selenium ide进行安装,安装完之后启用

NewImage

插件打开,主界面有点像charles,功能也有点类似,下面简单做一个登录我主页后台的操作,然后通过Selenium IDE将这部分浏览器操作导出为Java测试脚本

导出Java测试脚本,用的是JUnit

package com.lihuia.code.selenium;

import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.openqa.selenium.By;
import org.openqa.selenium.Dimension;
import org.openqa.selenium.JavascriptExecutor;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.chrome.ChromeDriver;

import java.util.HashMap;
import java.util.Map;

public class LoginTest {
private WebDriver driver;
private Map<String, Object> vars;
JavascriptExecutor js;
@Before
public void setUp() {
driver = new ChromeDriver();
js = (JavascriptExecutor) driver;
vars = new HashMap<String, Object>();
}
@After
public void tearDown() {
driver.quit();
}
@Test
public void untitled() {
driver.get("https://lihuia.com/wp-login.php?redirect_to=https%3A%2F%2Flihuia.com%2Fwp-admin%2F&reauth=1");
driver.manage().window().setSize(new Dimension(1547, 954));
driver.findElement(By.id("user_login")).sendKeys("FlyingCat");
driver.findElement(By.id("user_pass")).sendKeys("xxxxxxxx");
driver.findElement(By.id("wp-submit")).click();
driver.close();
}
}

这样就生成了登录操作的测试流程,前置新建了ChromeDriver,然后get打开登录页面,设置页面分辨率大小,配置用户名密码,最后提交,然后关闭退出driver

到了这一步,就比较清晰了,假如我们有一些异常测试,都可以直接在脚本里新增一个测试类,配置测试的参数,完成同种类型多个测试用例的覆盖;除此之外,Selenium IDE本身有很多操作功能,可以类似JMeter归总添加很多测试流程,甚至可以进行调试,执行

正常进行WEB前端自动化,可以将操作流程录制,然后在Selenium IDE或者导出来的测试脚本里添加总结测试用例,完成测试自动化的覆盖,至于Element的详情定位方式,可以根据WebDriver接口实现来完成

 

到了这里,假如我需要测试N种浏览器兼容性,假如一个一个执行,显然耗时非常久,甚至如果要测试不同操作系统上的不同浏览器,如果能分布式地在不同的节点上进行就完美了,正好Selenium Grid可以帮你完成

大致流程如下

UntitledImage

基本就是一个master-slave架构,hub节点收到消息后,分发到不同的agent上,根据各自节点的webdriver来执行对应浏览器操作

首先下载selenium-server包,官网直接下载即可,比如Mac上:

https://github.com/SeleniumHQ/selenium/releases/download/selenium-3.141.59/selenium-server-standalone-3.141.59.jar

Hub节点上,写一个配置文件,指定端口

{
  "port": 7777,
  "newSessionWaitTimeout": -1,
  "servlets" : [],
  "withoutServlets": [],
  "custom": {},
  "capabilityMatcher": "org.openqa.grid.internal.utils.DefaultCapabilityMatcher",
  "registry": "org.openqa.grid.internal.DefaultGridRegistry",
  "throwOnCapabilityNotPresent": true,
  "cleanUpCycle": 5000,
  "role": "hub",
  "debug": false,
  "browserTimeout": 0,
  "timeout": 1800
}

启动Hub节点进程

java -jar selenium-server-standalone-3.141.59.jar -role hub -hubConfig hub.json

Node节点上,写一个配置文件,指定要所有要测试的浏览器信息,以及hub信息

{
  "capabilities": [
    {
      "browserName": "chrome",
      "maxInstances": 5,
      "platform": "MAC",
      "webdriver.chrome.driver": "/usr/local/bin/chromedriver",
      "seleniumProtocol": "WebDriver"
    },{
      "browserName": "safari",
      "maxInstances": 5,
      "platform": "MAC",
      "webdriver.chrome.driver": "/usr/bin/safaridriver",
      "seleniumProtocol": "WebDriver"
    },{
      "browserName": "chrome",
      "maxInstances": 5,
      "platform": "LINUX",
      "webdriver.chrome.driver": "/usr/local/bin/chromedriver",
      "seleniumProtocol": "WebDriver"
    }
  ],
  "proxy": "org.openqa.grid.selenium.proxy.DefaultRemoteProxy",
  "maxSession": 5,
  "port": -1,
  "register": true,
  "registerCycle": 5000,
  "hub": "http://172.16.70.92:7777",
  "nodeStatusCheckTimeout": 5000,
  "nodePolling": 5000,
  "role": "node",
  "unregisterIfStillDownAfter": 60000,
  "downPollingLimit": 2,
  "debug": false,
  "servlets" : [],
  "withoutServlets": [],
  "custom": {}
}

启动Node节点进程

java -jar selenium-server-standalone-3.141.59.jar -role node -nodeConfig node.json

hub和node网络互通,没启动一个node节点的server,就会注册到hub节点服务里

最后,测试代码,和hub节点server进行交互,然后会被分发到不同的node节点找对应浏览器进行执行,返回的结果进行测试

package com.lihuia.code.selenium;

import org.openqa.selenium.Platform;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.remote.BrowserType;
import org.openqa.selenium.remote.DesiredCapabilities;
import org.openqa.selenium.remote.RemoteWebDriver;
import org.testng.Assert;
import org.testng.annotations.AfterTest;
import org.testng.annotations.BeforeTest;
import org.testng.annotations.Test;

import java.net.MalformedURLException;
import java.net.URL;

public class SeleniumGridTest {
WebDriver driver;
String hubURL, testURL;
DesiredCapabilities safariCapability, chromeCapability;

@BeforeTest
public void setUp() {
testURL = "https://google.com";
hubURL = "http://172.16.70.92:7777/wd/hub";
safariCapability = DesiredCapabilities.safari();
chromeCapability = DesiredCapabilities.chrome();
}

@AfterTest
public void afterTest() {
}

@Test(testName = "Safari in MAC")
public void safariTest() throws MalformedURLException {
safariCapability.setBrowserName(BrowserType.SAFARI);
safariCapability.setPlatform(Platform.MAC);
driver = new RemoteWebDriver(new URL(hubURL), safariCapability);
driver.get(testURL);
Assert.assertEquals(driver.getTitle(), "Google");
driver.quit();
}

@Test(testName = "Chrome in MAC")
public void chromeMacTest() throws MalformedURLException {
chromeCapability.setBrowserName(BrowserType.CHROME);
chromeCapability.setPlatform(Platform.MAC);
driver = new RemoteWebDriver(new URL(hubURL), chromeCapability);
driver.get(testURL);
Assert.assertEquals(driver.getTitle(), "Google");
driver.quit();
}

@Test(testName = "Chrome in Linux")
public void chromeLinuxTest() throws MalformedURLException {
chromeCapability.setBrowserName(BrowserType.CHROME);
chromeCapability.setPlatform(Platform.LINUX);
driver = new RemoteWebDriver(new URL(hubURL), chromeCapability);
driver.get(testURL);
Assert.assertEquals(driver.getTitle(), "Google");
driver.quit();
}
}

 

Selenium的几个主要模块就这些,webdriver提供了一个用户和浏览器自动化测试的桥梁,ide提供了一个可视化信息录制和回放的功能,grid提供了一套分布式自动化测试,能在不同的节点操作系统上,进行不同的浏览器操作

 

最后,没做过这方面的实现,不太清楚具体实质提升有多大,目前自我感觉这样测试也不是太稳健,投入回报率不高

发表回复