WD.js实战:统一Appium与Selenium,实现混合App自动化测试

发布时间:2026/6/29 3:23:43
WD.js实战:统一Appium与Selenium,实现混合App自动化测试 1. 项目概述为什么是WD.js如果你做过移动端自动化测试大概率对Appium和Selenium这两个名字不会陌生。Appium负责搞定手机AppSelenium负责搞定Web浏览器它们各自为王但当你需要在一个测试流程里同时操作App内的WebView比如微信小程序、H5页面和原生控件时麻烦就来了。你得在两个不同的客户端库、两套API之间来回切换脚本写得又长又乱维护起来简直是噩梦。我最近在做一个电商App的回归测试核心流程是启动App - 登录 - 进入商品详情页原生页面 - 点击“分享”跳转到微信小程序WebView - 在小程序内完成一些操作。最开始我用的是PythonAppium部分用appium-python-client一旦进入WebView就得用selenium重新初始化一个driver来操作两个driver实例的数据和上下文完全隔离状态同步、异常处理复杂得让人头疼。直到我遇到了WD.js。它不是一个全新的框架而是Appium团队官方维护的一个JavaScript客户端库。它的核心价值在于用一个统一的API同时封装了Appium用于移动端和Selenium用于Web端的协议。这意味着你只需要一个driver对象就能在原生App、混合App、移动端浏览器甚至桌面浏览器之间无缝切换底层协议细节被完美隐藏。对于前面那个电商测试场景WD.js让我用同一段脚本、同一种写法流畅地完成了从原生页面到WebView的所有操作脚本的简洁度和可维护性提升了不止一个档次。2. 环境搭建与核心依赖解析工欲善其事必先利其器。用WD.js之前得先把场子搭好。这里面的坑我几乎一个没落全踩过尤其是环境配置和版本兼容性问题。2.1 Node.js与包管理器选择WD.js基于Node.js所以第一步是安装Node.js。我的建议是不要用系统自带的包管理器安装比如apt-get或brew版本可能太旧或引发权限问题。直接去Node.js官网下载LTS长期支持版本安装包目前推荐18.x或20.x。安装后在终端运行node -v和npm -v确认版本。接下来是包管理器。npm是Node.js自带的够用但速度慢且依赖管理有时会出问题。我强烈推荐使用yarn或pnpm。yarn的缓存机制和确定性安装能极大避免“在我机器上是好的”这种问题。安装命令很简单npm install -g yarn或者用更快的pnpmnpm install -g pnpm项目初始化时用yarn init -y或pnpm init创建package.json文件。2.2 安装WD.js与测试框架WD.js是核心库但我们通常需要搭配一个测试框架如Mocha、Jest和一个断言库如Chai来组织测试用例。这里以yarn和Mocha为例# 安装WD.js yarn add wd # 安装测试框架和断言库 yarn add mocha chai --save-dev # 可选安装用于生成漂亮测试报告的库 yarn add mochawesome --save-dev这里有个关键点WD.js的版本。一定要查看其官方GitHub仓库的Release Notes。我曾经因为没注意用了较新的Node.js版本搭配一个旧的WD.js结果在建立WebSocket连接时一直报错。目前以当下时间点为例WD.js 1.x版本对Appium 2.x和Selenium 4.x支持较好。2.3 Appium Server的安装与配置WD.js是客户端它需要连接Appium Server由Appium Server再去驱动手机或模拟器。安装Appium Server有两种主流方式通过npm全局安装这是最直接的方式。npm install -g appium安装后在终端输入appium即可启动服务默认监听4723端口。但这种方式安装的Appium其相关的驱动如UiAutomator2驱动、XCUITest驱动需要单独安装。使用Appium Desktop这是一个带图形界面的工具非常适合新手和调试。它内置了Appium Server和Inspector用于定位元素。从官网下载安装即可。启动后点击“Start Server”按钮同样会启动在4723端口。注意无论哪种方式务必确保你安装的Appium版本是2.x。Appium 1.x已停止维护很多新特性和驱动在2.x上。安装后通过appium -v或Appium Desktop的关于页面查看版本。安装完Appium Server后必须安装对应平台的驱动。对于Android测试你需要uiautomator2驱动对于iOS需要xcuitest驱动。通过Appium 2.0的命令行工具安装# 安装Android驱动 appium driver install uiautomator2 # 安装iOS驱动 (需要在macOS系统下) appium driver install xcuitest可以通过appium driver list来查看已安装的驱动。2.4 移动端测试环境准备以Android为例这是最繁琐但也最重要的一步很多失败都源于环境问题。安装Java JDKAppium的部分组件需要Java环境。安装JDK 8或11LTS版本并配置JAVA_HOME环境变量。安装Android SDK推荐通过Android Studio安装它管理SDK版本和构建工具最方便。安装后需要配置ANDROID_HOME环境变量指向SDK根目录并把$ANDROID_HOME/platform-tools和$ANDROID_HOME/tools目录添加到系统的PATH中。准备设备可以是真机也可以是模拟器如Android Studio自带的AVD。真机需要开启“开发者选项”和“USB调试”。模拟器需要先创建并启动一个虚拟设备。关键检查在终端执行adb devices。如果能看到你的设备或模拟器列表说明ADB连接正常。这是后续所有操作的基础。3. WD.js核心API与第一个测试脚本环境配好了我们来写第一个脚本。WD.js的API设计是链式调用Promise-based理解它的几个核心方法就能上手。3.1 初始化Driver与基础配置首先创建一个测试文件比如first_test.js。我们需要引入WD库并配置连接Appium Server所需的能力Capabilities。Capabilities是一组键值对用来告诉Appium你要测试什么设备、什么应用。const wd require(wd); // 1. 配置Appium Server的地址 const serverConfig { hostname: localhost, // 如果Appium Server跑在本机 port: 4723, // 默认端口 path: /wd/hub // Appium 2.0的默认路径 }; // 2. 配置设备能力Capabilities // 这里以Android真机测试一个计算器App为例 const androidCapabilities { platformName: Android, // 平台 appium:automationName: UiAutomator2, // 自动化引擎必须指定 appium:deviceName: 你的设备名, // 通过 adb devices 查看 appium:platformVersion: 13, // 安卓系统版本 appium:appPackage: com.android.calculator2, // 被测App的包名 appium:appActivity: com.android.calculator2.Calculator, // 启动的Activity名 appium:noReset: true // 不重置应用状态避免每次清空数据 }; // 3. 创建driver实例 const driver wd.promiseChainRemote(serverConfig); // 4. 定义一个简单的测试套件使用async/await语法更清晰 (async function runTest() { try { // 初始化会话连接设备和App await driver.init(androidCapabilities); console.log(会话创建成功); // 在这里编写你的测试操作... // 例如点击数字按钮 const button7 await driver.elementById(com.android.calculator2:id/digit_7); await button7.click(); // 休眠2秒方便观察 await driver.sleep(2000); // 结束会话关闭App await driver.quit(); console.log(测试完成); } catch (error) { console.error(测试过程中发生错误, error); // 即使出错也尝试退出会话避免占用端口 await driver.quit(); } })();实操心得appium:前缀是W3C WebDriver标准格式在Appium 2.x中推荐使用。deviceName在Android上其实不是必须的但写上是个好习惯。获取appPackage和appActivity最准确的方法是在手机打开该App后在终端执行adb shell dumpsys window | grep mCurrentFocus。3.2 元素定位策略详解定位不到元素是自动化测试中最常见的问题。WD.js支持Selenium WebDriver的全部定位策略因为它的底层就是WebDriver协议。八大定位策略ByID(driver.elementById)首选。对于Android通常是resource-id对于iOS是accessibility id或name。Accessibility ID(driver.elementByAccessibilityId)在移动端这通常对应元素的content-desc或accessibilityIdentifier是为无障碍服务设计的非常适合做定位。XPath(driver.elementByXPath)功能最强大但性能最差且容易因UI改动而失效。不到万不得已不要用。如果要用尽量用相对路径和非索引依赖。Class Name(driver.elementByClassName)定位一类元素如android.widget.Button。通常一个界面上同类元素太多不精确。CSS Selector(driver.elementByCssSelector)仅适用于WebView中的网页元素在原生控件中无效。Android UIAutomator(driver.elementByAndroidUIAutomator)Android专属功能强大可以用UiAutomator API的表达式定位如new UiSelector().text(\确定\)。iOS Predicate String(driver.elementByIosUIAutomation)iOS专属类似XPath但效率更高语法如type \XCUIElementTypeButton\ AND label \提交\。iOS Class Chain(driver.elementByIosClassChain)iOS专属比Predicate更结构化定位速度更快。我的定位策略优先级首选 Accessibility ID如果开发同学规范地设置了这是最稳定、语义化最好的方式。次选 Resource ID (Android) / Name (iOS)原生提供的唯一标识。使用UIAutomator/Predicate当上述都没有时用平台专属的定位器比XPath可靠。XPath是最后的手段并且要拉着开发同学一起Review看能否为关键元素加上测试ID。3.3 常用交互API实战找到元素后就是与之交互。WD.js的API返回的是Promise使用async/await能让代码清晰得像写同步代码一样。// 点击操作 await driver.elementById(button_id).click(); // 输入文本会在输入前先清空 await driver.elementById(input_id).sendKeys(Hello WD.js); // 获取元素文本内容 let text await driver.elementById(text_view_id).text(); console.log(获取到的文本是, text); // 获取元素属性如是否启用、是否选中 let isEnabled await driver.elementById(button_id).getAttribute(enabled); let isChecked await driver.elementById(checkbox_id).getAttribute(checked); // 滑动操作从一点滑动到另一点 let startX 500, startY 1500, endX 500, endY 500; await driver.execute(mobile: swipe, { startX, startY, endX, endY, duration: 800 // 滑动持续时间单位毫秒 }); // 等待元素出现隐式等待不够用时 await driver.waitForElementById(loading_spinner, 10000); // 最多等10秒注意事项sendKeys对于有些输入框可能不会触发键盘的“完成”或“搜索”事件。这时候可能需要配合driver.pressKeyCode(66)66是回车键的键码来模拟按下回车。4. 实现Appium与Selenium的无缝切换这是WD.js的“杀手级”特性。场景你的App里嵌了一个WebView比如用于登录、支付或展示富文本你需要先操作原生部分然后进入WebView操作最后再返回原生。4.1 理解上下文Context在移动混合App中“上下文”是一个核心概念。默认启动后Driver处于NATIVE_APP上下文可以操作所有原生控件。当App内打开一个WebView时就会多出一个或多个WEBVIEW_*的上下文。关键步骤获取所有可用上下文let contexts await driver.contexts();。这会返回一个数组如[NATIVE_APP, WEBVIEW_com.yourapp.package]。切换到WebView上下文await driver.context(contexts[1]);。切换后所有WD.js的API就变成了操作网页DOM你可以像用Selenium测试PC浏览器一样使用CSS选择器、执行JavaScript等。切回原生上下文await driver.context(NATIVE_APP);。4.2 实战混合App登录流程假设一个App登录按钮是原生的点击后跳转到一个H5登录页面WebView输入账号密码后提交再跳回原生首页。const wd require(wd); const driver wd.promiseChainRemote(localhost, 4723); (async () { const caps { platformName: Android, appium:automationName: UiAutomator2, appium:deviceName: Android, appium:appPackage: com.example.myapp, appium:appActivity: .MainActivity, appium:chromedriverExecutable: /path/to/chromedriver, // 关键用于匹配WebView的Chrome版本 appium:autoWebview: false // 我们不希望自动切换 }; await driver.init(caps); try { // --- 步骤1: 原生界面操作 --- console.log(当前上下文, await driver.currentContext()); // 应该是 NATIVE_APP // 点击原生登录按钮 await driver.elementByAccessibilityId(login_button).click(); await driver.sleep(3000); // 等待WebView加载 // --- 步骤2: 切换到WebView --- let contexts await driver.contexts(); console.log(所有上下文, contexts); // 找到WEBVIEW开头的上下文 let webviewContext contexts.find(ctx ctx.startsWith(WEBVIEW_)); if (!webviewContext) { throw new Error(未找到WebView上下文); } await driver.context(webviewContext); console.log(切换到上下文, await driver.currentContext()); // 现在可以用Selenium的方式操作H5页面了 // 假设是标准的HTML输入框 await driver.elementByCssSelector(input[name\username\]).sendKeys(testuser); await driver.elementByCssSelector(input[name\password\]).sendKeys(password123); await driver.elementByCssSelector(button[type\submit\]).click(); // 等待登录完成页面跳转或消失 await driver.sleep(2000); // --- 步骤3: 切回原生上下文 --- await driver.context(NATIVE_APP); console.log(切回上下文, await driver.currentContext()); // 验证登录成功比如检查用户头像是否出现 let avatar await driver.waitForElementByAccessibilityId(user_avatar, 5000); if (avatar) { console.log(✅ 混合登录流程测试通过); } } catch (error) { console.error(❌ 测试失败, error); } finally { await driver.quit(); } })();避坑指南这里最大的坑是chromedriverExecutable。Appium需要通过ChromeDriver来驱动WebView。你必须确保安装的ChromeDriver版本与手机/模拟器内WebView或Chrome浏览器的版本兼容。最好通过appium --allow-insecure chromedriver_autodownload让Appium自动管理或者手动下载匹配的版本并在Capabilities中指定路径。5. 高级技巧与最佳实践当基础功能跑通后要写出健壮、可维护的测试脚本还需要一些高级技巧和架构思维。5.1 页面对象模型Page Object Pattern改造直接在测试用例里写driver.elementById(...).click()会很快导致代码难以维护。页面对象模型POP将页面元素和操作封装成类是业界标准的最佳实践。// pages/LoginPage.js class LoginPage { constructor(driver) { this.driver driver; // 定义元素选择器定位器 this.locators { usernameInput: #username, // WebView中用CSS passwordInput: by.accessibilityId(password_field), // 原生中用其他定位方式 submitButton: by.id(com.example.app:id/login_btn) }; } async switchToWebViewContext() { const contexts await this.driver.contexts(); const webview contexts.find(c c.startsWith(WEBVIEW_)); if (webview) await this.driver.context(webview); return this; } async switchToNativeContext() { await this.driver.context(NATIVE_APP); return this; } async login(username, password) { // 这个方法里封装了可能的上下文切换逻辑 await this.switchToWebViewContext(); await this.driver.elementByCss(this.locators.usernameInput).sendKeys(username); await this.driver.elementByCss(this.locators.passwordInput).sendKeys(password); await this.switchToNativeContext(); await this.driver.elementById(this.locators.submitButton).click(); } } module.exports LoginPage; // 在测试用例中使用 const LoginPage require(./pages/LoginPage); describe(登录测试, function() { it(应该能成功登录, async function() { const loginPage new LoginPage(driver); await loginPage.login(user, pass); // 添加断言... }); });这样当登录页面的UI发生变化时你只需要修改LoginPage.js文件中的locators和login方法所有测试用例都不需要改动。5.2 等待策略告别硬编码的sleepdriver.sleep(3000)是脆弱的网络或设备慢一点就可能失败。必须使用智能等待。隐式等待Implicit Wait在初始化driver后设置针对所有findElement操作生效。await driver.setImplicitWaitTimeout(10000); // 10秒但它对元素是否可点击、是否可见无效。显式等待Explicit Wait推荐使用。等待某个特定条件成立。const { until, By } require(selenium-webdriver); // WD.js兼容Selenium的等待条件 // 等待元素可见 let element await driver.wait( until.elementLocated(By.id(success_toast)), 15000, // 超时时间 成功提示框没有在15秒内出现 // 超时错误信息 ); // 等待元素可点击 await driver.wait(until.elementIsVisible(element), 5000); await driver.wait(until.elementIsEnabled(element), 5000);WD.js也自带了一些等待方法如driver.waitForElementById(id, timeout)但其条件比较单一。5.3 异常处理与截图记录测试失败时一张截图抵得上千行日志。WD.js可以方便地截图并配合测试框架的afterEach钩子使用。const fs require(fs); const path require(path); describe(某个功能模块, function() { // 每个测试用例结束后执行 afterEach(async function() { if (this.currentTest.state failed) { // 获取用例名作为截图文件名 const testName this.currentTest.title.replace(/\s/g, _); const timestamp new Date().toISOString().replace(/[:.]/g, -); const screenshotPath path.join(__dirname, screenshots, ${testName}_${timestamp}.png); // 创建目录如果不存在 const dir path.dirname(screenshotPath); if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true }); // 截图并保存 const screenshot await driver.takeScreenshot(); fs.writeFileSync(screenshotPath, screenshot, base64); console.log(⚠️ 测试失败截图已保存至: ${screenshotPath}); } }); it(一个可能会失败的测试, async function() { // ... 测试逻辑 throw new Error(故意失败); }); });同时在try...catch块中可以捕获更具体的错误如NoSuchElementError元素未找到、StaleElementReferenceError元素引用失效等并做出不同的处理或重试。5.4 并行测试与Grid配置当测试用例越来越多时串行执行太慢。可以利用Selenium Grid的思路搭建一个测试集群。启动Appium Server节点在多台机器或同一台机器的不同端口上启动多个Appium Server实例每个实例连接不同的手机或模拟器。配置WD.js连接GridWD.js的promiseChainRemote可以指定Grid Hub的地址。const driver wd.promiseChainRemote({ hostname: grid-hub-ip, port: 4444, // Grid Hub默认端口 path: /wd/hub });在Capabilities中指定目标设备通过appium:udid设备唯一标识来指定测试要运行在哪台设备上。你的测试框架如Mocha可以配合async库或原生Promise.all来并发执行多个测试任务。6. 常见问题排查与调试技巧在实际项目中你会遇到各种各样稀奇古怪的问题。这里记录了我踩过的一些典型深坑和解决方法。6.1 连接与会话问题问题现象可能原因排查步骤与解决方案Could not find a driver for...1. Appium Server未安装对应平台的驱动。2. Capabilities中automationName拼写错误或未指定。1. 运行appium driver list确认已安装uiautomator2或xcuitest。2. 运行appium driver install driver-name安装。3. 检查Capabilities确保有appium:automationName: UiAutomator2。Unable to connect to Appium server1. Appium Server未启动。2. 主机名或端口错误。3. 防火墙阻止。1. 在终端运行appium或启动Appium Desktop确认输出无报错并显示listening on 0.0.0.0:4723。2. 用浏览器或curl访问http://localhost:4723/wd/hub/status应返回JSON响应。3. 检查代码中的hostname和port。A new session could not be created1. 设备未连接或未授权。2. 指定的App包名/Activity名错误。3. 设备系统版本与驱动不兼容。1. 运行adb devices确认设备在线且状态为device。2. 真机检查是否弹出“允许USB调试”提示。3. 使用adb shell pm list packages和adb shell dumpsys activity确认正确的包名和Activity。4. 尝试更换模拟器或真机的系统版本。6.2 元素定位与交互问题问题现象可能原因排查步骤与解决方案NoSuchElementError1. 定位器写错了。2. 元素尚未加载出来。3. 元素在WebView里但上下文还在NATIVE_APP。1. 使用Appium Desktop的Inspector或Android Studio的Layout Inspector重新检查元素属性。2. 添加显式等待等待元素出现。3. 打印await driver.contexts()和await driver.currentContext()确认当前上下文是否正确。StaleElementReferenceError之前找到的元素因为页面刷新或重建已经失效。这是动态页面的常见问题。解决方案是“用时再找”不要长期持有元素对象。将元素定位代码封装在函数或Page Object的方法内部每次操作前重新查找。Element is not clickable at point1. 元素被遮挡如弹窗、蒙层。2. 元素实际不可点击如enabledfalse。3. 坐标点计算错误多见于滑动操作。1. 检查UI层级关闭可能的弹窗。2. 使用getAttribute(enabled)检查元素状态。3. 尝试用element.click()代替坐标点击。对于滑动使用mobile: swipe或mobile: scroll等更稳定的API。sendKeys不生效1. 焦点不在输入框。2. 输入框是自定义控件非标准输入框。3. 需要先清空内容。1. 先对输入框执行一次click()。2. 尝试使用driver.execute(mobile: type, {text: xxx})或driver.pressKeyCode模拟键盘输入。3. 先执行element.clear()。6.3 WebView相关疑难杂症问题现象可能原因排查步骤与解决方案找不到WEBVIEW_上下文1. App的WebView未开启调试模式。2. Appium的ChromeDriver版本不匹配。1.对于Android需要在App代码中为WebView设置WebView.setWebContentsDebuggingEnabled(true)并对调试版App生效。2.通用方案在Capabilities中设置appium:chromedriverExecutableDir指向一个包含多个版本ChromeDriver的目录或设置appium:chromedriverChromeMappingFile。3. 查看Appium Server日志搜索Chromedriver相关的错误信息。在WebView上下文中CSS定位器失效1. 页面内有iframe。2. 元素在Shadow DOM内。1. 使用driver.frame()切换到对应的iframe。2. 对于Shadow DOM需要使用JavaScript执行document.querySelector(...).shadowRoot.querySelector(...)来穿透查找。WD.js可以通过driver.executeScript()执行这段JS。切换回原生上下文后找不到原生元素页面发生了跳转或Activity切换旧的原生元素句柄失效。这是一个常见的“上下文陷阱”。切换回NATIVE_APP后如果页面已变需要重新查找元素。确保你的页面对象或操作逻辑在关键步骤后能重新初始化或查找元素。6.4 性能与稳定性调优减少不必要的截图截图操作很耗时只在失败或关键步骤时进行。使用noReset和fullResetnoReset: true可以避免每次测试都重装App节省大量时间。但需要注意测试间的数据隔离。fullReset: true则会在会话结束后彻底清除App数据。优化Capabilitiesappium:skipDeviceInitialization: 跳过一些设备初始化检查可以加快会话创建。appium:skipServerInstallation: 跳过在设备上安装Appium组件的步骤如果已经安装过。会话复用对于一组相关的测试用例考虑复用同一个Driver会话而不是每个用例都init和quit。但要做好清理工作避免用例间相互影响。调试时一定要多看Appium Server的日志。启动Appium时加上--log-level debug或者查看Appium Desktop的日志输出里面包含了客户端发送的每一个请求和服务器的响应是定位问题的金钥匙。