
1. 项目概述为什么需要摆脱jQuery进行自动化测试如果你和我一样是从那个“jQuery一统江湖”的年代走过来的前端开发者那么你肯定对$()这种简洁的语法无比熟悉。它曾是我们操作DOM、处理事件、发起Ajax请求的瑞士军刀。然而时代变了。现代前端开发早已转向了以React、Vue、Angular为代表的组件化框架原生JavaScriptES6的能力也今非昔比。如今一个项目如果还为了几个简单的DOM操作而引入整个jQuery库多少显得有些不合时宜尤其是在对性能和包体积有严格要求的场景下。在自动化测试领域这个矛盾尤为突出。我们使用Selenium、Puppeteer或Cypress等工具编写测试脚本目的是模拟用户操作验证应用功能。这些工具底层驱动的是真实的浏览器它们提供的API如document.querySelector、element.click()本身就是原生DOM API。如果在测试代码中混入jQuery相当于多了一层抽象不仅增加了依赖还可能因为jQuery版本与页面实际使用的版本冲突或者因为jQuery选择器与原生API的行为差异例如:visible伪类在原生API中不存在导致测试变得脆弱和难以维护。更关键的是理解并直接使用原生Web API进行测试能让你更深刻地理解浏览器是如何工作的测试脚本的意图也会更加清晰。当测试失败时你排查的是最底层的DOM交互问题而不是jQuery这个“中间商”可能引入的歧义。因此掌握用原生JavaScript编写健壮、高效的前端自动化测试是现代前端工程师和测试开发工程师的一项核心技能。2. 核心思路从jQuery思维到原生Web API思维要成功替换jQuery首先得完成一次思维转换。jQuery的本质是一个封装了DOM操作、事件、Ajax等功能的工具库。我们的目标就是用浏览器原生提供的标准Web API来实现同样的功能。2.1 选择器告别$()拥抱querySelector家族jQuery最强大的功能之一就是其选择器引擎。在原生JavaScript中我们主要使用document.querySelector和document.querySelectorAll。单元素选择$(‘#myId’)等价于document.querySelector(‘#myId’)。多元素选择$(‘.myClass’)等价于document.querySelectorAll(‘.myClass’)。注意querySelectorAll返回的是一个静态的NodeList类似于数组但不是真正的数组。如果需要使用数组方法可以将其转换为数组Array.from(document.querySelectorAll(‘.myClass’))。上下文内查找$(‘.parent’).find(‘.child’)可以转换为parentElement.querySelector(‘.child’)。实操心得querySelector在找不到元素时会返回null而jQuery会返回一个空对象。这在自动化测试中是个关键区别。测试中必须对null进行判空处理否则后续调用属性或方法会直接抛出错误导致测试中断。这是一个从“宽容”到“严格”的转变有助于写出更健壮的测试。2.2 DOM操作与属性jQuery提供了大量便捷的DOM操作方法如html()、text()、val()、attr()、prop()、addClass()等。这些在原生API中都有直接对应。获取/设置HTML内容$(‘div’).html()-divElement.innerHTML。获取/设置文本内容$(‘div’).text()-divElement.textContent。获取/设置表单值$(‘input’).val()-inputElement.value。获取/设置属性$(‘img’).attr(‘src’)-imgElement.getAttribute(‘src’)$(‘img’).attr(‘src’, ‘new.jpg’)-imgElement.setAttribute(‘src’, ‘new.jpg’)。获取/设置属性$(‘input’).prop(‘checked’)-inputElement.checked。类名操作$(‘div’).addClass(‘active’)-divElement.classList.add(‘active’)$(‘div’).removeClass(‘active’)-divElement.classList.remove(‘active’)$(‘div’).toggleClass(‘active’)-divElement.classList.toggle(‘active’)$(‘div’).hasClass(‘active’)-divElement.classList.contains(‘active’)注意事项classListAPI非常强大且直观是现代操作CSS类的首选方式。相比jQuery的字符串处理它更不容易出错。2.3 事件绑定与触发事件处理是自动化测试的核心。jQuery的.on()、.click()等方法深入人心。事件监听$(‘button’).on(‘click’, handler)-buttonElement.addEventListener(‘click’, handler)。事件移除$(‘button’).off(‘click’, handler)-buttonElement.removeEventListener(‘click’, handler)。触发事件$(‘button’).trigger(‘click’)-buttonElement.click()或buttonElement.dispatchEvent(new Event(‘click’))。实操心得addEventListener可以为一个元素的同一事件类型添加多个监听器而jQuery的.on()在内部也是这么管理的。在测试中我们更常用的是模拟用户交互比如直接调用element.click()或element.submit()这比触发一个合成事件更接近真实用户行为。对于复杂的输入如文件上传可能需要创建DataTransfer对象来模拟。2.4 样式操作jQuery的.css()方法可以获取和设置样式。获取样式$(‘div’).css(‘color’)-window.getComputedStyle(divElement).color。设置样式$(‘div’).css(‘color’, ‘red’)-divElement.style.color ‘red’。注意事项getComputedStyle获取的是最终计算后的样式值只读而element.style只能获取和设置内联样式。在测试中判断一个元素是否“可见”不能仅仅依赖display: none或visibility: hidden的内联样式而应结合getComputedStyle以及元素在视口中的位置如element.getBoundingClientRect()进行综合判断这是比jQuery的:visible伪类更精确的方式。2.5 Ajax与Fetch API在自动化测试中我们通常不直接测试Ajax调用而是测试Ajax调用后UI的更新。但理解其替代方案仍有必要。jQuery的$.ajax、$.get、$.post已被现代的fetchAPI或axios等库取代。基本请求$.get(‘/api/data’)-fetch(‘/api/data’).then(r r.json())。在测试中的意义在E2E测试中我们更倾向于等待某个DOM状态变化如一个加载提示消失、列表项出现来断言异步操作完成而不是直接拦截和断言网络请求。不过像Puppeteer这样的工具提供了直接拦截和模拟网络请求的能力。3. 实战演练用原生JavaScript重写Selenium测试用例理论说再多不如动手写一遍。让我们以一个经典的Selenium WebDriver测试场景为例分别用jQuery思维和原生JavaScript思维来实现并进行对比。假设我们要测试一个简单的待办事项应用在输入框中输入文本。点击“添加”按钮。验证新的待办事项项出现在列表中。点击该项的“删除”按钮验证其被移除。3.1 基于jQuery思维的测试代码假设页面已引入jQueryconst { Builder, By, until } require(‘selenium-webdriver’); (async function todoTestWithJQueryMindset() { const driver await new Builder().forBrowser(‘chrome’).build(); try { await driver.get(‘http://localhost:3000/todo-app’); // 1. 输入文本 - 依赖jQuery选择器语法但Selenium的By.css支持大部分 const input await driver.findElement(By.css(‘#todo-input’)); await input.sendKeys(‘Buy milk’); // 2. 点击添加按钮 const addButton await driver.findElement(By.css(‘#add-btn’)); await addButton.click(); // 3. 验证新项出现 - 这里开始体现jQuery思维用:contains找文本 // Selenium没有:contains我们得用XPath或遍历 await driver.wait(until.elementLocated(By.xpath(“//li[contains(text(), ‘Buy milk’)]”)), 5000); const newItem await driver.findElement(By.xpath(“//li[contains(text(), ‘Buy milk’)]”)); // 4. 找到该项目的删除按钮并点击 - 假设删除按钮是li内的一个.button-delete const deleteBtn await newItem.findElement(By.css(‘.button-delete’)); await deleteBtn.click(); // 5. 验证项目消失 - 等待元素不再存在于DOM await driver.wait(async () { const items await driver.findElements(By.xpath(“//li[contains(text(), ‘Buy milk’)]”)); return items.length 0; }, 5000); console.log(‘测试通过’); } finally { await driver.quit(); } })();这段代码虽然能用但XPath的使用contains(text(), …)有时不够稳定且逻辑上还是“找到包含某个文本的元素”这种jQuery式思路。3.2 基于原生Web API思维的测试代码现在我们用更接近原生DOM操作的思路来重写假设我们更了解页面结构或者可以与开发约定清晰的测试标识。const { Builder, By, until } require(‘selenium-webdriver’); (async function todoTestNative() { const driver await new Builder().forBrowser(‘chrome’).build(); try { await driver.get(‘http://localhost:3000/todo-app’); // 1. 输入文本 - 使用ID选择器最快速稳定 const input await driver.findElement(By.id(‘todo-input’)); await input.sendKeys(‘Buy milk’); // 2. 点击添加按钮 const addButton await driver.findElement(By.id(‘add-btn’)); await addButton.click(); // 3. 验证新项出现 - 定位到列表容器查找其最后一个子元素 const todoList await driver.findElement(By.id(‘todo-list’)); // 等待列表中有至少一个项目 await driver.wait(until.elementLocated(By.css(‘#todo-list li’)), 5000); // 获取所有项目 const items await todoList.findElements(By.css(‘li’)); const lastItem items[items.length - 1]; // 验证最后一个项目的文本内容 const itemText await lastItem.getText(); if (!itemText.includes(‘Buy milk’)) { throw new Error(新增项目文本不符期望包含“Buy milk”实际是“${itemText}”); } // 4. 找到该项目的删除按钮并点击 - 使用更具体的data-testid属性 const deleteBtn await lastItem.findElement(By.css(‘[data-testid”delete-btn”]’)); await deleteBtn.click(); // 5. 验证项目消失 - 等待列表的子元素数量减少 await driver.wait(async () { const currentItems await todoList.findElements(By.css(‘li’)); return currentItems.length items.length - 1; }, 5000); console.log(‘测试通过’); } catch (error) { console.error(‘测试失败:’, error.message); // 这里可以附加截图等操作 throw error; } finally { await driver.quit(); } })();核心差异与优势分析选择器策略原生思维优先使用By.id因为ID在页面中应是唯一的选择速度最快也最稳定。其次是By.css它支持丰富的CSS选择器足以覆盖绝大多数场景。尽量避免使用复杂且性能较差的XPath除非没有其他选择。定位上下文第二段代码先定位到父容器#todo-list再在其中查找li。这更符合DOM的树形结构也减少了全局搜索的范围提高了选择器的性能和准确性。使用自定义数据属性我们为删除按钮添加了>// testUtils.js class NativeTestUtils { constructor(driver) { this.driver driver; } // 1. 稳定的元素查找带重试和超时 async findElement(selector, timeout 10000) { const element await this.driver.wait( until.elementLocated(By.css(selector)), timeout, 无法在${timeout}ms内找到元素: ${selector} ); // 额外等待元素变得可交互可见且可点击 await this.driver.wait( until.elementIsVisible(element), timeout ); return element; } async findElementById(id, timeout 10000) { return this.findElement(#${id}, timeout); } async findElementByTestId(testId, timeout 10000) { return this.findElement([data-testid”${testId}”], timeout); } // 2. 安全的点击操作 async click(selector) { const element await this.findElement(selector); try { await element.click(); } catch (error) { // 如果常规点击失败尝试使用JavaScript执行点击应对某些覆盖层遮挡 await this.driver.executeScript(‘arguments[0].click();’, element); } } // 3. 表单操作 async type(selector, text) { const element await this.findElement(selector); await element.clear(); // 先清空 await element.sendKeys(text); } // 4. 获取文本、属性等 async getText(selector) { const element await this.findElement(selector); return await element.getText(); } async getAttribute(selector, attrName) { const element await this.findElement(selector); return await element.getAttribute(attrName); } // 5. 断言函数 async assertTextContains(selector, expectedText) { const actualText await this.getText(selector); if (!actualText.includes(expectedText)) { throw new Error(断言失败元素”${selector}”的文本”${actualText}”不包含”${expectedText}”); } } async assertElementVisible(selector) { const element await this.findElement(selector); const isDisplayed await element.isDisplayed(); if (!isDisplayed) { throw new Error(断言失败元素”${selector}”不可见); } } async assertElementNotPresent(selector, timeout 5000) { try { await this.driver.wait(async () { const elements await this.driver.findElements(By.css(selector)); return elements.length 0; }, timeout); } catch (error) { throw new Error(断言失败元素”${selector}”在${timeout}ms后仍然存在); } } } module.exports NativeTestUtils;使用这个工具库重写我们的待办事项测试const { Builder } require(‘selenium-webdriver’); const NativeTestUtils require(‘./testUtils’); (async function todoTestWithUtils() { const driver await new Builder().forBrowser(‘chrome’).build(); const utils new NativeTestUtils(driver); try { await driver.get(‘http://localhost:3000/todo-app’); // 1. 输入文本 await utils.type(‘#todo-input’, ‘Buy milk’); // 2. 点击添加按钮 await utils.click(‘#add-btn’); // 3. 验证新项出现 - 通过data-testid定位列表和最后一项 const todoList await utils.findElementByTestId(‘todo-list’); const items await todoList.findElements(By.css(‘[data-testid”todo-item”]’)); const lastItem items[items.length - 1]; await utils.assertTextContains(‘[data-testid”todo-item”]:last-child’, ‘Buy milk’); // 4. 点击删除按钮 const deleteBtn await lastItem.findElement(By.css(‘[data-testid”delete-btn”]’)); await deleteBtn.click(); // 5. 验证项目消失 - 断言列表项数量减少 await driver.wait(async () { const currentItems await todoList.findElements(By.css(‘[data-testid”todo-item”]’)); return currentItems.length items.length - 1; }, 5000); console.log(‘测试通过’); } catch (error) { console.error(‘测试失败:’, error.message); // 可以在这里加入截图逻辑await driver.takeScreenshot().then(...) throw error; } finally { await driver.quit(); } })();可以看到测试代码变得非常清晰和声明式几乎像是在描述用户操作步骤。所有底层的等待、查找、错误处理都被封装在工具类中。5. 处理复杂场景与常见问题5.1 等待策略超越sleep在自动化测试中硬编码的sleep是万恶之源。它让测试变得缓慢且不可靠。原生WebDriver提供了更智能的等待方式。隐式等待driver.manage().setTimeouts({ implicit: 5000 })。设置后所有findElement操作都会在指定时间内轮询查找元素直到找到为止。但全局设置可能影响性能且对元素可见、可点击等条件无效。显式等待这是推荐的方式。使用driver.wait(condition, timeout, message)。// 等待元素可见并可点击 const button await driver.wait( until.elementIsVisible(driver.findElement(By.id(‘myBtn’))), 10000 ); // 等待某个条件成立 await driver.wait(async () { const text await driver.findElement(By.id(‘status’)).getText(); return text ‘加载完成’; }, 15000);在我们的工具函数findElement中就结合使用了until.elementLocated和until.elementIsVisible这是一种非常实用的模式。5.2 处理iframe如果页面中嵌入了iframe你需要先切换到iframe的上下文中才能操作其中的元素。// 通过ID或索引切换到iframe await driver.switchTo().frame(‘iframe-id’); // 或者 await driver.switchTo().frame(0); // 在iframe内操作 const iframeButton await utils.findElement(‘#button-inside-iframe’); await iframeButton.click(); // 操作完成后切回主文档 await driver.switchTo().defaultContent();5.3 执行JavaScript代码有时直接执行JavaScript是最高效或唯一的选择例如获取复杂的计算样式、滚动到某个元素、或者模拟一些特殊事件。// 滚动到元素可见 const element await driver.findElement(By.id(‘footer’)); await driver.executeScript(‘arguments[0].scrollIntoView(true);’, element); // 获取计算后的样式 const color await driver.executeScript( ‘return window.getComputedStyle(arguments[0]).color’, element ); // 设置元素属性如触发文件上传 const fileInput await driver.findElement(By.css(‘input[type”file”]’)); await driver.executeScript( “arguments[0].style.display ‘block’;”, // 让隐藏的file input可见如果需要 fileInput ); await fileInput.sendKeys(‘/absolute/path/to/file.txt’);注意事项executeScript是强大的但应谨慎使用。过度依赖它会让测试脱离真实的用户交互流程。优先使用WebDriver提供的标准API如sendKeys,click只有在标准API无法实现或存在缺陷时才考虑使用executeScript。5.4 文件上传与下载文件上传通常通过input type”file”元素实现直接用sendKeys传入文件绝对路径即可如上例所示。文件下载则更复杂一些因为涉及浏览器与操作系统的交互。一种常见做法是配置浏览器选项指定一个固定的下载目录然后在测试中检查该目录下是否出现了预期的文件。const chrome require(‘selenium-webdriver/chrome’); let options new chrome.Options(); let prefs { ‘download.default_directory’: ‘/path/to/downloads’ }; options.setUserPreferences(prefs); const driver await new Builder() .forBrowser(‘chrome’) .setChromeOptions(options) .build(); // … 执行触发下载的操作 … // 然后使用Node.js的fs模块检查文件 const fs require(‘fs’); const path require(‘path’); const downloadedFile path.join(‘/path/to/downloads’, ‘expected-file.pdf’); await driver.wait(() fs.existsSync(downloadedFile), 30000);6. 集成与最佳实践6.1 与测试框架集成纯WebDriver脚本缺乏结构化的测试组织和断言报告。将其与Mocha、Jest等测试框架结合是行业标准。// test/todo.spec.js const { Builder } require(‘selenium-webdriver’); const { describe, it, before, after } require(‘mocha’); const { expect } require(‘chai’); const NativeTestUtils require(‘../utils/testUtils’); describe(‘待办事项应用’, function() { this.timeout(30000); // 设置全局超时 let driver; let utils; before(async () { driver await new Builder().forBrowser(‘chrome’).build(); utils new NativeTestUtils(driver); }); after(async () { await driver.quit(); }); it(‘应该能成功添加和删除待办项’, async () { await driver.get(‘http://localhost:3000/todo-app’); await utils.type(‘#todo-input’, ‘Mocha Test Item’); await utils.click(‘#add-btn’); // 使用Chai断言库 const itemText await utils.getText(‘[data-testid”todo-item”]:last-child’); expect(itemText).to.include(‘Mocha Test Item’); const initialCount (await driver.findElements(By.css(‘[data-testid”todo-item”]’))).length; await utils.click(‘[data-testid”todo-item”]:last-child [data-testid”delete-btn”]’); // 等待数量减少 await driver.wait(async () { const newCount (await driver.findElements(By.css(‘[data-testid”todo-item”]’))).length; return newCount initialCount - 1; }, 5000); const finalCount (await driver.findElements(By.css(‘[data-testid”todo-item”]’))).length; expect(finalCount).to.equal(initialCount - 1); }); });使用npm test运行测试Mocha会提供清晰的测试报告。6.2 页面对象模式对于中大型项目强烈推荐使用页面对象模式。它将每个页面或组件的元素定位和操作封装成一个类使测试代码更易读、易维护。// pages/TodoPage.js class TodoPage { constructor(driver) { this.driver driver; this.utils new NativeTestUtils(driver); } get input() { return this.driver.findElement(By.id(‘todo-input’)); } get addButton() { return this.driver.findElement(By.id(‘add-btn’)); } get items() { return this.driver.findElements(By.css(‘[data-testid”todo-item”]’)); } getLastItemDeleteBtn() { return this.driver.findElement(By.css(‘[data-testid”todo-item”]:last-child [data-testid”delete-btn”]’)); } async navigate() { await this.driver.get(‘http://localhost:3000/todo-app’); } async addItem(text) { await this.utils.type(‘#todo-input’, text); await this.addButton.click(); } async getLastItemText() { const items await this.items; const lastItem items[items.length - 1]; return await lastItem.getText(); } async deleteLastItem() { await this.getLastItemDeleteBtn().click(); } async getItemCount() { const items await this.items; return items.length; } } module.exports TodoPage;然后在测试中这样使用// test/todoWithPageObject.spec.js const { Builder } require(‘selenium-webdriver’); const { describe, it, before, after } require(‘mocha’); const { expect } require(‘chai’); const TodoPage require(‘../pages/TodoPage’); describe(‘待办事项应用 (页面对象模式)’, function() { let driver; let todoPage; before(async () { driver await new Builder().forBrowser(‘chrome’).build(); todoPage new TodoPage(driver); }); after(async () { await driver.quit(); }); it(‘应该能成功添加和删除待办项’, async () { await todoPage.navigate(); const initialCount await todoPage.getItemCount(); await todoPage.addItem(‘Page Object Test’); const newItemText await todoPage.getLastItemText(); expect(newItemText).to.include(‘Page Object Test’); await todoPage.deleteLastItem(); // 等待并断言数量减少 await driver.wait(async () { return (await todoPage.getItemCount()) initialCount; }, 5000); const finalCount await todoPage.getItemCount(); expect(finalCount).to.equal(initialCount); }); });6.3 持续集成与无头浏览器在CI/CD流水线中运行测试通常使用无头浏览器以节省资源。const chrome require(‘selenium-webdriver/chrome’); const { Builder } require(‘selenium-webdriver’); async function createHeadlessDriver() { let options new chrome.Options(); options.addArguments(‘—headlessnew’); // Chrome 112 推荐使用new options.addArguments(‘—no-sandbox’); options.addArguments(‘—disable-dev-shm-usage’); // 在Docker等受限环境中很有用 const driver await new Builder() .forBrowser(‘chrome’) .setChromeOptions(options) .build(); return driver; }踩坑提醒无头模式下一些行为可能与有头模式略有不同例如视口大小、某些Web API的可用性。务必在无头环境下充分测试。另外记得在测试失败时截取屏幕快照这对于在CI中调试至关重要。我们的工具类可以很容易地扩展一个截图方法。7. 总结与展望从依赖jQuery到拥抱原生Web API进行前端自动化测试不仅仅是一次技术栈的切换更是一次测试理念的升级。它迫使你更深入地理解DOM标准编写出更精准、更稳定、与框架无关的测试代码。通过封装工具函数、采用页面对象模式、并与现代测试框架和CI流程集成你可以构建出一套强大、可维护的前端自动化测试体系。这个过程初期可能会觉得有些繁琐毕竟jQuery的链式调用和隐式迭代确实很简洁。但长远来看原生方案带来的稳定性、清晰度和性能优势是巨大的。当你的测试套件能够快速、可靠地在每次提交时运行并成功捕获回归缺陷时你就会觉得这一切的投入都是值得的。最终你的测试代码将不再是一堆脆弱的“脚本”而是项目代码库中一份坚实、可信赖的资产。