Skip to content

跨平台设备管理方案seleniumgrid

跨平台设备管理方案 Selenium Grid

简介

Selenium Grid 是 Selenium 的三大组件之一,它可以在多台机器上并行运行测试,集中管理不同的浏览器版本和浏览器配置。通过将客户端命令发送到远程浏览器的实例, Selenium Grid 允许在远程计算机 (虚拟或真实) 上执行 WebDriver 脚本. 它旨在提供一种在多台计算机上并行运行测试的简便方法。

官方文档:https://www.selenium.dev/

Grid4 新特性

Selenium Grid4 在更新之后增加了 3 个特性:

  1. Hub 和 Node 使用同一个 jar 服务。
  2. 架构优化,在 Selenium Grid 4 版本的全新架构中划分成了组件:Router、Distributor、Node、Session Map、Session Queue、Event Bus。
  3. 支持不同的运行模式,Selenium 4 支持三种网格类型,包括 Standalone Mode 独立模式、Classical Grid 经典网格模式、Fully Distributed 完全分布式

下面这张图是官方提供的 Selenium Grid4 的架构图:

Selenium Grid4架构图

从图中可以看到 Selenium Grid 包括六大组件,分别是:

  • Router 路由器
  • Distributor 分发服务器
  • Session Map 会话映射
  • Node 测试结点
  • New Session Queue 新的会话队列
  • Event Bus 事件总线

SeleniumGrid 实现原理

SeleniumGrid 的实现原理为通过 SeleniumGridHub 作为一个中控中心。将客户端的请求分发到各个 Node 执行对应的脚本。只要保证 Node 具备相应的环境即可。如此一来,可以实现:

  • 无需在本地安装 Appium Server 环境也可以进行 App 自动化测试。
  • 无需在本地安装多个浏览器环境也可以进行 Web 的兼容性测试。

uml diagram

应用场景

  • 实现分布式执行测试:提高执行效率。同样数量的测试用例,分布式运行的运行效率是单机运行的数倍。
  • 浏览器/手机设备兼容性问题:在测试过程中,同样的测试用例需要在不同的设备/浏览器上面运行,以保证软件能够在不同的设备或浏览器中正常运行。

如何使用

Selenium Grid 针对于不同的应用场景有多种运行方式。

  1. 单机运行:独立模式。
  2. 单机运行:经典网格模式。
  3. 多系统运行:分发模式。

接下来将以最常用的网格模式进行演示,SeleniuimGrid 的使用也并不复杂,在使用过程中,主要分为以下 2 个步骤:

  1. 安装与部署,将 Hub 与 Node 部署好,确定可以被正常调用。
  2. 代码调用,直接通过调用 SeleniuimGrid 服务实现自动化测试。

安装与部署

uml diagram

由以上的架构图也可以看出,SeleniumGrid 的安装与部署主要分为 Hub 与 Node:

  • Hub: 中控中心,只需要部署一个。
  • Node: 子节点,可以部署多个。子节点可以为浏览器,也可以为 AppiumServer。

关于安装与部署的方式,常用的主要就是两种: Jar 包以及 Docker 部署。以下为官方文档的部署资料:

  • Selenium 官方文档地址(Jar 包部署):https://www.selenium.dev/documentation/grid/getting_started/

  • SeleniumDocker 官方文档地址(Docker 包部署):https://github.com/SeleniumHQ/docker-selenium?tab=readme-ov-file#hub-and-nodes

Jar 包安装与部署(推荐)
  1. 安装 Hub:java -jar selenium-server-<version>.jar hub。安装成功之后,可以访问http://127.0.0.1:4444/,即可进入 Grid 页面。

  2. 添加 Node,如果需要添加端口则使用 --port:java -jar selenium-server-<version>.jar node --port 5555

  3. 添加成功之后,即可在 hub 页面看到对应的设备。
Docker 安装与部署

注意:如果不了解 Docker 的同学,建议使用 Jar 安装。排错较为麻烦。

  • 安装 Hub。
# 创建 grid 网络
docker network create grid
# --net 将Hub容器连接到grid网络
docker run -d -p 4442-4444:4442-4444 --net grid --name selenium-hub selenium/hub:4.16.1-20231219
  • 添加浏览器 Node。
# 创建
docker run -d --net grid -e SE_EVENT_BUS_HOST=selenium-hub \
    --shm-size="2g" \
    -e SE_EVENT_BUS_PUBLISH_PORT=4442 \
    -e SE_EVENT_BUS_SUBSCRIBE_PORT=4443 \
    selenium/node-chrome
# 创建 firefox 浏览器节点
docker run -d --net grid -e SE_EVENT_BUS_HOST=selenium-hub \
    --shm-size="2g" \
    -e SE_EVENT_BUS_PUBLISH_PORT=4442 \
    -e SE_EVENT_BUS_SUBSCRIBE_PORT=4443 \
    selenium/node-firefox
添加 Appium Node

AppiumServer 也可以作为一个 Node 注册到 Hub 中,以此实现移动端的多设备自动化测试。

  1. 准备 Appium 配置文件(如下所示), 并通过命令启动 appium 服务:appium --config appium1.yml
server:
# Appium 服务启动端口
port: 4724
# 使用的Driver
use-drivers:
    - uiautomator2
  1. 创建节点配置文件。
# 指定节点端口
[server]
port = 5580
[node]
detect-drivers = false
[relay]
# 不能配置本地网络,比如 localhost 或者 127.0.0.1 因为网络设置为grid 没有和宿主机共享网络,所以访问不到
url = "http://172.16.4.187:4723"
status-endpoint = "/status"
# 配置信息,会展示在hub界面
configs = [
    "2", "{\"platformName\": \"Android\", \"appium:udid\": \"emulator-5556\", \"appium:automationName\": \"uiautomator2\"}"
]
  1. 启动服务,添加节点:
# 使用jar包的方式添加节点
java -jar /path/to/selenium.jar node --config node1.toml
# 使用Docker包的方式添加节点
docker run  -d --net grid -e SE_EVENT_BUS_HOST=selenium-hub \
    -e SE_EVENT_BUS_PUBLISH_PORT=4442 \
    -e SE_EVENT_BUS_SUBSCRIBE_PORT=4443 \
    -v 配置文件地址/node1.toml:/opt/bin/config.toml \
    -v /var/run/docker.sock:/var/run/docker.sock \
    selenium/node-docker

Grid部署效果

SeleniumGrid 实战案例

通过 Grid 实现调用不同的 App 设备

在代码中,通过添加配置信息,以及在初始化 Driver 的时候输入 Hub 的地址,即可实现通过 Grid 调用不同的设备。

  • Python 版本:

class TestGrid:

    @pytest.mark.parametrize(
        "udid", 
        ["emulator-5554", "emulator-5556"]
    )
    def test_grid(self, udid):
        """
        grid 调用 appium 实现自动化测试
        :return:
        """
        caps = {}
        # 设置 app 安装平台
        caps["appium:platformName"] = "Android"
        # 设置driver
        caps["appium:automationName"] = "UiAutomator2"
        # 设置 app 的包名
        caps["appium:appPackage"] = "com.android.settings"
        # 设置 app 启动页
        caps["appium:appActivity"] = ".Settings"
        # 设置 udid 可以指定hub分配到指定设备
        caps["appium:udid"] = udid
        # 不清空缓存
        caps["appium:noReset"] = True
        # 设置以下两个参数来控制启动app和关闭掉app
        caps["appium:forceAppLaunch"] = True
        caps["appium:shouldTerminateApp"] = True
        options = AppiumOptions().load_capabilities(caps)
        driver = webdriver.Remote(
            "http://localhost:4444", 
            options=options
        )
        time.sleep(5)
        driver.quit()

  • Java 版本:
public class TestGrid {
    @Test
    public void grid() throws InterruptedException {
        UiAutomator2Options uiAutomator2Options = new UiAutomator2Options()
                .setPlatformName("Android")
                .setAutomationName("uiautomator2")
                // 使用不同的udid连接不同的设备
                .setUdid("emulator-5556")
                .setNoReset(true)
                .amend("appium:appPackage", "com.android.settings")
                .amend("appium:appActivity", ".Settings")
                .amend("appium:forceAppLaunch", true)
                .amend("appium:shouldTerminateApp", true);
        try {
            // 连接Selenium Hub
            // 连接Selenium Hub
            RemoteWebDriver driver = new RemoteWebDriver(new URL("http://127.0.0.1:4444"), uiAutomator2Options);
            driver.findElement(AppiumBy.xpath("//*[@text='Battery']")).click();
            Thread.sleep(2000);
            // 必须要进行quit操作,否则grid 运行的时候会占用session
            driver.quit();
            } catch (MalformedURLException e) {
            throw new RuntimeException(e);
        }
    }
}
多浏览器运行代码

除了可以调用不同的移动端设备外,还可以通过此方式实现多浏览器的 web 端兼容性测试。

  • Python 版本:

创建测试文件 test_multi_node.py 示例代码如下:

from time import sleep
from selenium import webdriver
from selenium.webdriver.common.by import By

class TestMultiNode:
    def setup_method(self):
        options = webdriver.ChromeOptions()
        self.driver = webdriver.Remote(
            command_executor='http://10.1.1.104:4444',
            options=options
        )

    def test_multinode1(self):
        # 打开 baidu 页
        self.driver.get("http://www.baidu.com")
        # 向输入框中输入
        self.driver.find_element(By.ID, 'kw').send_keys("selenium")
        # 点击搜索框
        self.driver.find_element(By.ID, 'su').click()
        # 等待一秒
        sleep(1)
        # 断言输入内容在页面中
        assert "selenium" in self.driver.page_source

    def test_multinode2(self):
        # 打开 baidu 页
        self.driver.get("http://www.baidu.com")
        # 向输入框中输入
        self.driver.find_element(By.ID, 'kw').send_keys("appium")
        # 点击搜索框
        self.driver.find_element(By.ID, 'su').click()
        # 等待一秒
        sleep(1)
        # 断言输入内容在页面中
        assert "appium" in self.driver.page_source

    def test_multinode3(self):
        # 打开 baidu 页
        self.driver.get("http://www.baidu.com")
        # 向输入框中输入
        self.driver.find_element(By.ID, 'kw').send_keys("pytest")
        # 点击搜索框
        self.driver.find_element(By.ID, 'su').click()
        # 等待一秒
        sleep(1)
        # 断言输入内容在页面中
        assert "pytest" in self.driver.page_source

    def test_multinode4(self):
        # 打开 baidu 页
        self.driver.get("http://www.baidu.com")
        # 向输入框中输入
        self.driver.find_element(By.ID, 'kw').send_keys("requests")
        # 点击搜索框
        self.driver.find_element(By.ID, 'su').click()
        # 等待一秒
        sleep(1)
        # 断言输入内容在页面中
        assert "requests" in self.driver.page_source

    def test_multinode5(self):
        # 打开 baidu 页
        self.driver.get("http://www.baidu.com")
        # 向输入框中输入
        self.driver.find_element(By.ID, 'kw').send_keys("java")
        # 点击搜索框
        self.driver.find_element(By.ID, 'su').click()
        # 等待一秒
        sleep(1)
        # 断言输入内容在页面中
        assert "java" in self.driver.page_source

    def teardown_method(self):
        self.driver.quit()

为了模拟多浏览器并发运行,使用 pytest 的插件 pytest-xdist 实现分布式并发执行方式,提前安装 pytest-xdist 插件,然后使用命令执行用例。打开命令提示行或者终端,使用cd 命令进入到文件所在路径,然后执行 pytest test_multi_node.py -n 3 --alluredir ./results命令。执行完用例之后,会把测试报告结果统一汇总到results 目录中。

  • Java 版本:
/*
 * @Author: 霍格沃兹测试开发学社-盖盖
 * @Desc: '更多测试开发技术探讨,请访问:https://ceshiren.com/t/topic/15860'
 */
package com.ceshiren.nodes;

public class EdgeTest{
    private WebDriver webDriver;
    static final Logger logger = getLogger(lookup().lookupClass());
    static DesiredCapabilities caps = new DesiredCapabilities();

    @Test
    public void edge(TestInfo testInfo) throws MalformedURLException, InterruptedException {
        WebDriver webDriver = initDriver(testInfo, Platform.ANY);
        webDriver.get("https://ceshiren.com/");
        String title = webDriver.getTitle();
        logger.info("MicrosoftEdge浏览器打开 {}", title);
        sleep(4000);
        webDriver.quit();
        assertThat(title,containsString("测试人社区"));

    }
    public WebDriver initDriver(TestInfo testInfo, Platform platform) throws MalformedURLException {
        String browser = testInfo.getTestMethod().map(Method::getName).get();
        logger.info("浏览器为:{}",browser);
//        caps.setPlatform(platform);
        caps.setCapability(CapabilityType.PLATFORM_NAME,platform);
        if("chrome".equals(browser)){
            caps.setBrowserName("chrome");
//            caps.setCapability(CapabilityType.BROWSER_NAME,"chrome");
            ChromeOptions options = new ChromeOptions();
            options.merge(caps);
        }else if ("firefox".equals(browser)){
            caps.setBrowserName("firefox");
            FirefoxOptions options = new FirefoxOptions();
            options.merge(caps);
        }else if ("edge".equals(browser)){
            caps.setBrowserName("MicrosoftEdge");
            EdgeOptions options = new EdgeOptions();
            options.merge(caps);
        }else if ("safari".equals(browser)){
            caps.setBrowserName("safari");
            SafariOptions options = new SafariOptions();
            options.merge(caps);
        }
        webDriver = new RemoteWebDriver(new URL("http://127.0.0.1:4444"),caps);
        webDriver.manage().timeouts().implicitlyWait(Duration.ofSeconds(5));
        return webDriver;
    }
}
  • suite 套件运行代码:
@Suite
@SelectPackages("com.ceshiren.nodes")
public class SuiteTest {
}

总结

  • SeleniumGrid 安装与部署
  • SeleniumGrid 调用不同 App 设备
  • SeleniumGrid 调用多浏览器