还在为端到端测试头疼?Nightwatch.js 让你的测试效率提升10倍的秘密
前言
当你的 Web 应用从一个小项目成长为拥有数百个页面的复杂系统时,如何确保每一次功能迭代都不会破坏现有功能?手动测试已经无法满足快速迭代的需求,而选择一款合适的端到端测试框架就成了关键。
今天要介绍的这个开源项目,已经被全球超过 50 万开发者使用,并且在 GitHub 上获得了超过 1.2 万颗星——它就是 Nightwatch.js。
在接下来的文章中,我将带你从零开始,深入了解这个强大的端到端测试框架。无论你是刚接触自动化测试的新手,还是希望提升测试效率的老手,这篇教程都能给你带来实质性的帮助。
一、为什么 Nightwatch.js 值得关注
1.1 行业痛点与 Nightwatch 的解决方案
在前端开发领域,自动化测试一直是公认的难题。传统的测试方式存在以下痛点:
传统测试痛点:
├── 测试编写复杂,学习曲线陡峭
├── 测试执行缓慢,CI/CD 集成困难
├── 跨浏览器兼容性测试难以实现
├── 测试代码难以维护和复用
└── 调试困难,问题定位耗时
Nightwatch.js 针对这些问题提供了优雅的解决方案:
Nightwatch 的核心优势:
├── 简洁的语法 - 使用自然语言风格的 API
├── 底层基于 W3C WebDriver 协议 - 标准化且稳定
├── 内置并行测试执行 - 大幅提升测试速度
├── 丰富的内置命令 - 覆盖 95% 的常见测试场景
├── 优秀的 CI/CD 集成 - 支持所有主流 CI 平台
└── 强大的等待机制 - 告别不稳定的 flaky tests
1.2 Nightwatch.js 是什么
Nightwatch.js 是一个端到端测试框架,使用 Node.js 编写,专为 Web 应用设计。它的核心特点包括:
- 基于 WebDriver 协议:遵循 W3C WebDriver 标准,确保跨浏览器兼容性
- 简洁的测试语法:使用类似英语的可读语句编写测试
- 内置测试运行器:无需额外配置即可运行测试
- 丰富的断言库:内置大量常用断言,也支持自定义
- Page Object 模式支持:让测试代码更易维护
1.3 谁在使用 Nightwatch.js
Nightwatch.js 被众多知名企业和开源项目采用:
用户案例:
├── Stripe - 全球领先的支付处理平台
├── Airbnb - 民宿预订巨头
├── GoDaddy - 域名和托管服务提供商
├── Shopify - 电商建站平台
├── GitHub - 全球最大代码托管平台
└── 众多中小企业和独立开发者
这些企业的选择证明了 Nightwatch.js 在生产环境中的可靠性和稳定性。
二、环境搭建:从零开始配置 Nightwatch
2.1 前置要求
在开始之前,请确保你的开发环境满足以下要求:
环境要求:
├── Node.js 16.x 或更高版本
├── npm 8.x 或更高版本(或 yarn/pnpm)
├── Chrome、Firefox、Edge 或 Safari 浏览器
└── Git(用于版本控制)
验证环境配置:
# 检查 Node.js 版本
node --version
# 输出应该类似于: v18.17.0
# 检查 npm 版本
npm --version
# 输出应该类似于: 9.6.7
2.2 创建项目结构
让我们从创建一个新的测试项目开始:
# 创建项目目录
mkdir nightwatch-tutorial
cd nightwatch-tutorial
# 初始化 npm 项目
npm init -y
# 安装 Nightwatch.js
npm install nightwatch --save-dev
安装完成后,项目结构应该是这样的:
nightwatch-tutorial/
├── node_modules/
│ └── nightwatch/
├── package.json
├── package-lock.json
└── tests/ # 稍后我们会创建这个目录
2.3 配置 Nightwatch
Nightwatch.js 需要一个配置文件来指定测试设置和浏览器选项。创建 nightwatch.conf.js 文件:
// nightwatch.conf.js
// Nightwatch 配置文件
module.exports = {
// 源代码文件夹路径
src_folders: ["tests"],
// 输出文件夹
output_folder: "reports",
// 页面对象模型文件夹
page_objects_path: ["page-objects"],
// WebDriver 设置
webdriver: {
start_process: true,
server_path: "",
port: 9515,
cli_args: [
"--no-sandbox",
"--disable-setuid-sandbox"
]
},
// 测试设置
test_settings: {
// 默认设置
default: {
launch_url: "http://localhost",
desiredCapabilities: {
browserName: "chrome",
"goog:chromeOptions": {
args: [
"--headless",
"--disable-gpu",
"--window-size=1280,800"
]
}
}
},
// Firefox 设置
firefox: {
desiredCapabilities: {
browserName: "firefox",
"moz:firefoxOptions": {
args: ["-headless"]
}
}
},
// Safari 设置
safari: {
desiredCapabilities: {
browserName: "safari"
}
},
// Chrome with GUI(用于调试)
chrome: {
desiredCapabilities: {
browserName: "chrome",
"goog:chromeOptions": {
// 移除 headless 模式以便观察
args: ["--window-size=1280,800"]
}
}
}
}
};
// ============================================
// 如果你想使用 ChromeDriver 自动下载和启动
// 可以使用 nightwatch 的内置 WebDriver 管理
// ============================================
2.4 使用 WebDriver Manager(推荐方式)
对于大多数用户来说,使用 Nightwatch 内置的 WebDriver 管理器会更加方便。更新配置文件:
// nightwatch.conf.js
const { faker } = require('@faker-js/faker');
module.exports = {
src_folders: ["tests"],
output_folder: "reports",
page_objects_path: ["page-objects"],
// 使用 Nightwatch 内置的 WebDriver
webdriver: {
start_process: true,
use_ssl: true,
port: 9515,
server_path: require('chromedriver').path,
cli_args: [
'--whitelisted-ips=',
'--no-sandbox',
'--disable-gpu'
]
},
test_settings: {
default: {
launch_url: "https://www.example.com",
desiredCapabilities: {
browserName: "chrome",
"goog:chromeOptions": {
args: [
"--headless",
"--no-sandbox",
"--disable-dev-shm-usage",
"--disable-gpu",
"--window-size=1280,800"
]
}
}
}
}
};
2.5 添加测试脚本到 package.json
为了方便运行测试,在 package.json 中添加脚本:
{
"name": "nightwatch-tutorial",
"version": "1.0.0",
"description": "Nightwatch.js 端到端测试教程",
"main": "index.js",
"scripts": {
"test": "nightwatch",
"test:chrome": "nightwatch --env chrome",
"test:firefox": "nightwatch --env firefox",
"test:debug": "nightwatch --env chrome --debug",
"test:parallel": "nightwatch --parallel"
},
"keywords": ["nightwatch", "e2e", "testing"],
"author": "",
"license": "MIT",
"devDependencies": {
"nightwatch": "^2.6.0"
}
}
2.6 验证安装
创建第一个简单的测试文件来验证安装是否成功:
mkdir tests
// tests/demo.test.js
// 第一个 Nightwatch 测试
describe('验证 Nightwatch 安装', function() {
// 测试用例
it('应该能够打开网页', function(browser) {
// 打开指定 URL
browser.url('https://example.com');
// 等待页面加载完成
browser.waitForElementVisible('body');
// 获取页面标题
browser.assert.title('Example Domain');
// 结束测试
browser.end();
});
});
运行测试:
npm test
如果一切配置正确,你应该能看到类似以下的输出:
[验证 Nightwatch 安装] 测试开始
✓ 应该能够打开网页 (1.234s)
测试完成
1 个测试通过,0 个测试失败
总耗时: 2.345s
三、核心功能详解
3.1 浏览器操作
Nightwatch.js 提供了丰富的浏览器操作命令,让我们逐一了解。
3.1.1 导航操作
describe('导航操作演示', function() {
it('基本导航操作', function(browser) {
// 打开指定 URL
browser.url('https://www.example.com');
// 等待页面完全加载
browser.waitForElementVisible('body');
// 后退到上一页
browser.back();
// 前进到下一页
browser.forward();
// 刷新当前页面
browser.refresh();
// 获取当前 URL
browser.url(function(result) {
console.log('当前 URL:', result.value);
});
// 关闭浏览器
browser.end();
});
it('导航到相对路径', function(browser) {
browser.url('https://www.example.com');
// 导航到相对路径
browser.navigateTo('/page1');
// 导航到绝对路径
browser.navigateTo('https://www.example.com/page2');
browser.end();
});
});
3.1.2 窗口和框架操作
describe('窗口和框架操作', function() {
it('窗口操作', function(browser) {
browser.url('https://www.example.com');
// 获取当前窗口句柄
browser.windowHandles(function(handles) {
console.log('窗口数量:', handles.value.length);
});
// 切换到新窗口(假设已打开)
browser.switchWindow('windowTitle');
// 关闭当前窗口
browser.closeWindow();
// 全屏窗口
browser.maximizeWindow();
// 最小化窗口
browser.minimizeWindow();
// 设置窗口大小
browser.resizeWindow(1280, 720);
// 获取窗口大小
browser.getWindowSize(function(result) {
console.log('窗口尺寸:', result.value);
});
browser.end();
});
it('iframe 操作', function(browser) {
browser.url('https://www.example.com');
// 切换到 iframe
browser.frame('iframeName');
// 或者使用索引
browser.frame(0);
// 或者使用 iframe 元素
browser.frame('[name="iframeName"]');
// 在 iframe 中执行操作
browser.click('#iframe-element');
// 切换回主文档
browser.frame(null);
browser.end();
});
});
3.2 元素定位与交互
3.2.1 元素定位策略
Nightwatch 支持多种元素定位策略:
describe('元素定位策略', function() {
it('使用 CSS 选择器定位', function(browser) {
browser.url('https://www.example.com');
// CSS 选择器
browser.click('#submit-button'); // ID
browser.click('.btn-primary'); // Class
browser.click('div.container'); // 标签名
browser.click('ul.nav > li:first-child'); // 组合选择器
browser.end();
});
it('使用 XPath 定位', function(browser) {
browser.url('https://www.example.com');
// XPath 绝对路径
browser.click('xpath=/html/body/div[2]/form/input');
// XPath 相对路径
browser.click('xpath=//input[@type="submit"]');
// XPath 包含文本
browser.click('xpath=//button[contains(text(),"提交")]');
// XPath 多个条件
browser.click('xpath=//div[@class="form"]//input[@name="email"]');
browser.end();
});
it('使用文本内容定位', function(browser) {
browser.url('https://www.example.com');
// 链接文本
browser.click('link=关于我们');
// 部分链接文本
browser.click('linkText=了解更多');
browser.end();
});
it('使用 DOM 属性定位', function(browser) {
browser.url('https://www.example.com');
// name 属性
browser.setValue('[name="username"]', 'testuser');
// data-testid 属性
browser.click('[data-testid="submit-btn"]');
// 多个属性组合
browser.click('[type="submit"][disabled]');
browser.end();
});
});
3.2.2 鼠标操作
describe('鼠标操作', function() {
it('基本鼠标操作', function(browser) {
browser.url('https://www.example.com');
// 单击
browser.click('#button');
// 双击
browser.doubleClick('#double-click-button');
// 右键单击
browser.rightClick('#context-menu-button');
// 悬停
browser.moveToElement('#hover-element');
browser.end();
});
it('拖拽操作', function(browser) {
browser.url('https://www.example.com');
// 将元素拖拽到目标元素
browser.dragAndDrop('#source-element', '#target-element');
// 或者分步进行
browser.perform(function(done) {
// 获取元素位置
this.element('id', 'source-element', function(result) {
var elementId = result.value.ELEMENT;
// 执行拖拽
this.moveTo(elementId, 5, 5)
.mouseDown()
.moveTo(null, 100, 100)
.mouseUp();
done();
});
});
browser.end();
});
});
3.2.3 键盘操作
describe('键盘操作', function() {
it('输入操作', function(browser) {
browser.url('https://www.example.com');
// 设置输入值(清除现有内容后输入)
browser.setValue('#search-input', 'nightwatch');
// 追加输入(不清除现有内容)
browser.appendValue('#search-input', ' tutorial');
// 清除输入
browser.clearValue('#search-input');
// 提交表单
browser.submitForm('#search-form');
browser.end();
});
it('特殊按键', function(browser) {
browser.url('https://www.example.com');
browser.setValue('#search-input', 'hello');
// 模拟回车键
browser.keys(browser.Keys.ENTER);
// 模拟 Tab 键
browser.keys(browser.Keys.TAB);
// 模拟 Ctrl+A(全选)
browser.keys([browser.Keys.CONTROL, 'a']);
// 模拟 Ctrl+C(复制)
browser.keys([browser.Keys.CONTROL, 'c']);
// 模拟 Ctrl+V(粘贴)
browser.keys([browser.Keys.CONTROL, 'v']);
// 模拟退格键
browser.keys(browser.Keys.BACKSPACE);
// 模拟 Escape 键
browser.keys(browser.Keys.ESCAPE);
browser.end();
});
it('组合键示例', function(browser) {
browser.url('https://www.example.com');
// Ctrl+A 全选
browser.setValue('#input', 'Hello World');
browser.click('#input');
browser.keys([browser.Keys.CONTROL, 'a']);
// Ctrl+C 复制
browser.keys([browser.Keys.CONTROL, 'c']);
// 点击另一个输入框
browser.click('#another-input');
// Ctrl+V 粘贴
browser.keys([browser.Keys.CONTROL, 'v']);
browser.end();
});
});
3.3 断言与验证
断言是测试的核心部分,Nightwatch 提供了丰富的断言方法。
3.3.1 元素断言
describe('元素断言', function() {
it('可见性断言', function(browser) {
browser.url('https://www.example.com');
// 断言元素可见
browser.assert.visible('#visible-element');
// 断言元素不可见
browser.assert.hidden('#hidden-element');
// 断言元素存在
browser.assert.elementPresent('#existing-element');
// 断言元素不存在
browser.assert.elementNotPresent('#non-existing-element');
browser.end();
});
it('文本断言', function(browser) {
browser.url('https://www.example.com');
// 断言元素包含指定文本
browser.assert.containsText('#element', '预期文本');
// 断言元素文本等于指定值
browser.assert.textEquals('#element', '精确文本');
// 断言元素文本符合正则表达式
browser.assert.matchesText('#element', /^\d{4}-\d{2}-\d{2}$/);
browser.end();
});
it('值断言', function(browser) {
browser.url('https://www.example.com');
// 断言输入框的值
browser.assert.value('#input', '预期值');
// 断言输入框的值包含
browser.assert.valueContains('#input', '部分值');
// 断言复选框被选中
browser.assert.checked('#checkbox');
// 断言复选框未被选中
browser.assert.not.checked('#checkbox');
browser.end();
});
it('属性断言', function(browser) {
browser.url('https://www.example.com');
// 断言元素具有指定属性
browser.assert.attributeEquals('#element', 'data-id', '123');
// 断言元素属性包含
browser.assert.attributeContains('#element', 'class', 'active');
// 断言元素有 disabled 属性
browser.assert.attributePresent('#element', 'disabled');
browser.end();
});
it('CSS 断言', function(browser) {
browser.url('https://www.example.com');
// 断言 CSS 属性
browser.assert.cssProperty('#element', 'display', 'none');
// 断言 CSS 属性值包含
browser.assert.cssProperty('#element', 'color', 'rgb(255, 0, 0)');
browser.end();
});
});
3.3.2 高级断言
describe('高级断言', function() {
it('使用 expect API', function(browser) {
browser.url('https://www.example.com');
// expect 风格的断言
browser.expect.element('#title').text.to.equal('页面标题');
browser.expect.element('#title').text.to.contain('页面');
browser.expect.element('#count').text.to.match(/^\d+$/);
// expect 可见性
browser.expect.element('#modal').to.be.visible;
browser.expect.element('#modal').to.not.be.visible;
// expect 属性
browser.expect.element('#link').to.have.attribute('href').which.equals('https://example.com');
browser.expect.element('#input').to.have.attribute('disabled').which.is.empty;
// expect 类名
browser.expect.element('#element').to.have.class('active');
browser.expect.element('#element').to.not.have.class('hidden');
browser.end();
});
it('自定义断言', function(browser) {
browser.url('https://www.example.com');
// 使用 assert.title
browser.assert.title('Example Domain');
// 使用 assert.url
browser.assert.urlContains('/page');
// 使用 assert.url.equals
browser.assert.urlEquals('https://www.example.com/page');
// 使用 assert.cookie
browser.assert.cookiePresent('session_id');
browser.assert.cookieEquals('session_id', 'abc123');
// 使用 assert.jsEnabled
browser.assert.jsEnabled();
// 使用 assert.cssAnimation
browser.assert.cssAnimation('#element');
browser.end();
});
});
3.4 等待机制
等待是端到端测试中最重要也最容易出问题的地方。Nightwatch 提供了智能的等待机制。
3.4.1 隐式等待
describe('隐式等待设置', function() {
// 在配置文件中全局设置
before(browser => {
// 设置全局超时时间
browser.globals.waitForConditionTimeout = 5000;
browser.globals.waitForConditionPollInterval = 100;
});
it('自动等待元素出现', function(browser) {
browser.url('https://www.example.com');
// Nightwatch 会自动等待元素变为可见
browser.waitForElementVisible('#async-element');
// 等待元素出现在 DOM 中
browser.waitForElementPresent('#new-element');
// 等待元素消失
browser.waitForElementNotVisible('#loading');
browser.end();
});
});
3.4.2 显式等待
describe('显式等待', function() {
it('使用 waitForElementVisible', function(browser) {
browser.url('https://www.example.com');
// 等待元素可见,最多等待 10 秒,每 500ms 检查一次
browser.waitForElementVisible('#target', 10000, 500);
browser.end();
});
it('使用 waitForElementNotVisible', function(browser) {
browser.url('https://www.example.com');
// 等待元素不可见(加载完成)
browser.waitForElementNotVisible('.loading-spinner', 30000);
browser.end();
});
it('等待条件满足', function(browser) {
browser.url('https://www.example.com');
// 等待 JavaScript 条件满足
browser.waitForCondition('return document.readyState === "complete"', 10000);
// 等待自定义函数返回 true
browser.waitForCondition(function() {
return this.execute(function() {
return window.someAsyncData !== undefined;
});
}, 10000);
browser.end();
});
it('等待属性变化', function(browser) {
browser.url('https://www.example.com');
// 等待元素有特定属性
browser.waitForElementAttribute('#button', 'disabled', null, 5000);
// 等待元素有特定类名
browser.waitForElementPresent('#element.active', 5000);
browser.end();
});
});
3.4.3 自定义等待条件
describe('自定义等待条件', function() {
it('等待 AJAX 请求完成', function(browser) {
browser.url('https://www.example.com');
// 定义一个等待 AJAX 完成的函数
function waitForAjaxComplete(browser) {
return browser.execute(function() {
return jQuery.active === 0;
});
}
// 使用自定义条件等待
browser.perform(function(done) {
this.waitForCondition(waitForAjaxComplete, 10000, function() {
console.log('AJAX 请求完成');
done();
});
});
browser.end();
});
it('等待元素文本变化', function(browser) {
browser.url('https://www.example.com');
// 获取初始文本
var initialText;
browser.getText('#status', function(result) {
initialText = result.value;
});
// 点击按钮触发更新
browser.click('#update-button');
// 等待文本变化
browser.perform(function(done) {
var maxAttempts = 20;
var attempt = 0;
var checkText = function() {
this.getText('#status', function(result) {
if (result.value !== initialText) {
done();
} else if (attempt < maxAttempts) {
attempt++;
setTimeout(checkText.bind(this), 500);
} else {
done(new Error('文本未在预期时间内变化'));
}
});
};
checkText.call(this);
});
browser.end();
});
});
3.5 Page Object 模式
Page Object 是测试代码组织的最佳实践,它将页面元素和操作封装成独立的对象。
3.5.1 创建 Page Objects
// page-objects/google-search.js
// Google 搜索页面对象
class GoogleSearchPage {
constructor(browser) {
this.browser = browser;
// 元素选择器
this.elements = {
searchInput: 'textarea[name="q"]',
searchButton: 'input[name="btnK"]',
searchResults: '#search',
firstResult: '#search a:first-child',
logo: '#hplogo'
};
}
// 打开页面
open() {
this.browser.url('https://www.google.com');
return this;
}
// 等待页面加载
waitForPageLoad() {
this.browser.waitForElementVisible(this.elements.searchInput);
return this;
}
// 执行搜索
search(keyword) {
this.browser
.setValue(this.elements.searchInput, keyword)
.click(this.elements.searchButton);
return this;
}
// 等待搜索结果加载
waitForResults() {
this.browser.waitForElementVisible(this.elements.searchResults);
return this;
}
// 获取搜索结果数量
getResultCount() {
return this.browser.elements(this.elements.searchResults + ' a');
}
// 获取第一个结果的标题
getFirstResultTitle() {
var title;
this.browser.getText(this.elements.firstResult, function(result) {
title = result.value;
});
return title;
}
// 检查是否搜索成功
assertSearchSuccessful() {
this.browser.assert.visible(this.elements.searchResults);
return this;
}
}
// 导出页面对象
module.exports = GoogleSearchPage;
3.5.2 在测试中使用 Page Objects
// tests/google-search.test.js
// 使用 Page Object 的测试示例
const GoogleSearchPage = require('../page-objects/google-search');
describe('Google 搜索功能测试', function() {
// 创建页面对象实例
let googleSearch;
before(browser => {
googleSearch = new GoogleSearchPage(browser);
});
it('应该能够搜索并显示结果', function(browser) {
// 使用 Page Object 的方法
googleSearch
.open()
.waitForPageLoad()
.search('Nightwatch.js')
.waitForResults()
.assertSearchSuccessful();
// 获取结果并验证
var resultCount = googleSearch.getResultCount();
console.log('找到', resultCount.value.length, '个搜索结果');
browser.end();
});
it('验证第一个搜索结果包含关键词', function(browser) {
googleSearch
.open()
.waitForPageLoad()
.search('automation testing');
googleSearch.waitForResults();
// 验证第一个结果
var firstTitle = googleSearch.getFirstResultTitle();
console.log('第一个结果:', firstTitle);
browser.end();
});
});
3.5.3 高级 Page Object 模式
// page-objects/base-page.js
// 基础页面对象类
class BasePage {
constructor(browser) {
this.browser = browser;
}
// 打开页面
open(path) {
this.browser.url(path);
return this;
}
// 等待页面加载
waitForPageLoad() {
this.browser.waitForElementVisible('body');
return this;
}
// 点击元素
click(selector) {
this.browser.click(selector);
return this;
}
// 输入文本
setValue(selector, value) {
this.browser.setValue(selector, value);
return this;
}
// 获取元素文本
getText(selector) {
return this.browser.getText(selector);
}
// 等待元素可见
waitForVisible(selector, timeout) {
this.browser.waitForElementVisible(selector, timeout);
return this;
}
// 断言元素可见
assertVisible(selector) {
this.browser.assert.visible(selector);
return this;
}
// 滚动到元素
scrollIntoView(selector) {
this.browser.execute(function(selector) {
document.querySelector(selector).scrollIntoView();
}, selector);
return this;
}
// 获取当前 URL
getCurrentUrl() {
return this.browser.url(function(result) {
return result.value;
});
}
}
// ============================================
// 子页面继承基础页面
// ============================================
class LoginPage extends BasePage {
constructor(browser) {
super(browser);
this.elements = {
usernameInput: '#username',
passwordInput: '#password',
submitButton: 'button[type="submit"]',
errorMessage: '.error-message',
rememberMe: '#remember-me'
};
}
open() {
super.open('/login');
return this;
}
login(username, password, rememberMe = false) {
this.browser
.setValue(this.elements.usernameInput, username)
.setValue(this.elements.passwordInput, password);
if (rememberMe) {
this.browser.click(this.elements.rememberMe);
}
this.browser.click(this.elements.submitButton);
return this;
}
waitForError() {
this.browser.waitForElementVisible(this.elements.errorMessage);
return this;
}
}
class DashboardPage extends BasePage {
constructor(browser) {
super(browser);
this.elements = {
welcomeMessage: '.welcome',
logoutButton: 'a[href="/logout"]',
userMenu: '.user-menu',
profileLink: 'a[href="/profile"]'
};
}
open() {
super.open('/dashboard');
return this;
}
waitForLoad() {
this.browser.waitForElementVisible(this.elements.welcomeMessage);
return this;
}
getWelcomeMessage() {
return this.getText(this.elements.welcomeMessage);
}
logout() {
this.click(this.elements.logoutButton);
return this;
}
}
// 导出所有页面对象
module.exports = {
BasePage,
LoginPage,
DashboardPage
};
3.5.4 Page Object 目录结构
page-objects/
├── base-page.js # 基础页面类
├── google-search.js # Google 搜索页面
├── login-page.js # 登录页面
├── dashboard-page.js # 仪表盘页面
├── page-objects.json # 页面对象配置(可选)
└── components/ # 可复用的组件
├── header.js # 页头组件
├── footer.js # 页脚组件
└── modal.js # 模态框组件
四、实战教程:完整测试场景
4.1 项目实战:电商网站测试
让我们通过一个完整的实战项目来学习 Nightwatch 的实际应用。我们将测试一个假设的电商网站「ShopDemo」。
4.1.1 创建项目结构
# 创建完整的项目结构
mkdir -p tests/{pages,components,tests,utils}
mkdir -p reports/screenshots
mkdir -p page-objects/{pages,components}
# 安装额外的依赖
npm install @faker-js/faker chai --save-dev
4.1.2 配置文件
// nightwatch.conf.js
module.exports = {
// 源码目录
src_folders: ['tests'],
// 页面对象目录
page_objects_path: ['page-objects'],
// 输出目录
output_folder: 'reports',
// 自定义命令目录
custom_commands_path: ['commands'],
// 自定义断言目录
custom_assertions_path: ['assertions'],
// WebDriver 设置
webdriver: {
start_process: true,
server_path: '',
port: 9515,
cli_args: [
'--no-sandbox',
'--disable-setuid-sandbox',
'--disable-dev-shm-usage'
]
},
// 测试设置
test_settings: {
default: {
launch_url: 'https://shopdemo.com',
screenshots: {
enabled: true,
on_failure: true,
path: 'reports/screenshots'
},
desiredCapabilities: {
browserName: 'chrome',
'goog:chromeOptions': {
args: [
'--headless',
'--no-sandbox',
'--disable-gpu',
'--window-size=1280,800'
]
}
}
},
// Chrome 调试模式
chrome_debug: {
launch_url: 'https://shopdemo.com',
desiredCapabilities: {
browserName: 'chrome',
'goog:chromeOptions': {
args: [
'--window-size=1280,800',
'--auto-open-devtools-for-tabs'
]
}
}
},
// Firefox 设置
firefox: {
desiredCapabilities: {
browserName: 'firefox',
'moz:firefoxOptions': {
args: ['-headless']
}
}
}
}
};
4.1.3 登录页面对象
// page-objects/pages/login-page.js
// 登录页面对象
module.exports = {
elements: {
// 表单元素
emailInput: 'input[name="email"]',
passwordInput: 'input[name="password"]',
rememberMeCheckbox: 'input[name="remember"]',
submitButton: 'button[type="submit"]',
// 错误提示
emailError: '[data-testid="email-error"]',
passwordError: '[data-testid="password-error"]',
formError: '.form-error-message',
// 链接
forgotPasswordLink: 'a[href="/forgot-password"]',
registerLink: 'a[href="/register"]',
// Logo
logo: '.logo img'
},
commands: [{
// 打开登录页面
open() {
return this.navigate();
},
// 输入邮箱
setEmail(email) {
return this.setValue('@emailInput', email);
},
// 输入密码
setPassword(password) {
return this.setValue('@passwordInput', password);
},
// 点击记住我
checkRememberMe() {
return this.click('@rememberMeCheckbox');
},
// 点击提交按钮
submit() {
return this.click('@submitButton');
},
// 完整登录流程
login(email, password, rememberMe = false) {
this.setEmail(email);
this.setPassword(password);
if (rememberMe) {
this.checkRememberMe();
}
return this.submit();
},
// 等待表单加载
waitForForm() {
return this.waitForElementVisible('@emailInput');
},
// 获取邮箱错误信息
getEmailError() {
return this.getText('@emailError');
},
// 获取表单错误信息
getFormError() {
return this.getText('@formError');
},
// 检查是否显示邮箱错误
hasEmailError() {
return this.isVisible('@emailError');
}
}]
};
4.1.4 产品列表页面对象
// page-objects/pages/product-list-page.js
// 产品列表页面对象
module.exports = {
elements: {
// 搜索相关
searchInput: 'input[name="search"]',
searchButton: 'button[type="submit"]',
// 分类筛选
categoryMenu: '.category-menu',
categoryLinks: '.category-menu a',
// 价格筛选
minPriceInput: 'input[name="min-price"]',
maxPriceInput: 'input[name="max-price"]',
applyPriceButton: 'button.apply-price',
// 产品卡片
productCards: '.product-card',
productNames: '.product-card .product-name',
productPrices: '.product-card .product-price',
productImages: '.product-card img',
// 第一个产品
firstProduct: '.product-card:first-child',
firstProductName: '.product-card:first-child .product-name',
firstProductPrice: '.product-card:first-child .product-price',
firstProductImage: '.product-card:first-child img',
addToCartButton: '.product-card:first-child .add-to-cart',
// 分页
pagination: '.pagination',
nextPageButton: '.pagination .next',
currentPage: '.pagination .current',
// 加载状态
loadingSpinner: '.loading-spinner',
noResultsMessage: '.no-results'
},
commands: [{
// 搜索产品
search(keyword) {
return this
.clearValue('@searchInput')
.setValue('@searchInput', keyword)
.click('@searchButton');
},
// 等待搜索结果
waitForResults() {
return this
.waitForElementNotVisible('@loadingSpinner')
.waitForElementVisible('@productCards');
},
// 获取产品数量
getProductCount(callback) {
return this.elements('@productCards', callback);
},
// 获取第一个产品名称
getFirstProductName() {
return this.getText('@firstProductName');
},
// 获取第一个产品价格
getFirstProductPrice() {
return this.getText('@firstProductPrice');
},
// 点击第一个产品的添加到购物车按钮
addFirstProductToCart() {
return this.click('@addToCartButton');
},
// 筛选分类
selectCategory(categoryName) {
return this
.click('@categoryMenu')
.click(`xpath=//a[contains(text(),"${categoryName}")]`);
},
// 设置价格范围
setPriceRange(min, max) {
return this
.clearValue('@minPriceInput')
.setValue('@minPriceInput', min)
.clearValue('@maxPriceInput')
.setValue('@maxPriceInput', max)
.click('@applyPriceButton');
},
// 点击下一页
goToNextPage() {
return this.click('@nextPageButton');
},
// 获取当前页码
getCurrentPage() {
return this.getText('@currentPage');
},
// 悬停在产品上
hoverOnFirstProduct() {
return this.moveToElement('@firstProduct');
},
// 检查是否有结果
hasResults() {
return this.isVisible('@productCards');
}
}]
};
4.1.5 购物车页面对象
// page-objects/pages/cart-page.js
// 购物车页面对象
module.exports = {
elements: {
// 购物车内容
cartItems: '.cart-item',
emptyCartMessage: '.empty-cart-message',
// 第一个商品
firstItem: '.cart-item:first-child',
firstItemName: '.cart-item:first-child .item-name',
firstItemPrice: '.cart-item:first-child .item-price',
firstItemQuantity: '.cart-item:first-child .item-quantity input',
firstItemRemoveButton: '.cart-item:first-child .remove-item',
// 数量控制
quantityInput: '.quantity-input',
decreaseButton: '.quantity-decrease',
increaseButton: '.quantity-increase',
// 金额计算
subtotal: '.cart-subtotal',
shipping: '.cart-shipping',
tax: '.cart-tax',
total: '.cart-total',
// 操作按钮
continueShoppingButton: 'button.continue-shopping',
checkoutButton: 'button.checkout',
clearCartButton: 'button.clear-cart',
// 优惠券
couponInput: 'input[name="coupon"]',
applyCouponButton: 'button.apply-coupon',
couponSuccess: '.coupon-success',
couponError: '.coupon-error'
},
commands: [{
// 等待购物车加载
waitForLoad() {
return this.waitForElementVisible('.cart-container');
},
// 获取商品数量
getItemCount() {
return this.elements('@cartItems', result => {
return result.value.length;
});
},
// 获取第一个商品名称
getFirstItemName() {
return this.getText('@firstItemName');
},
// 获取小计
getSubtotal() {
return this.getText('@subtotal');
},
// 获取总价
getTotal() {
return this.getText('@total');
},
// 设置商品数量
setQuantity(itemSelector, quantity) {
return this.clearValue(itemSelector)
.setValue(itemSelector, quantity);
},
// 增加数量
increaseQuantity() {
return this.click('@increaseButton');
},
// 减少数量
decreaseQuantity() {
return this.click('@decreaseButton');
},
// 移除第一个商品
removeFirstItem() {
return this.click('@firstItemRemoveButton');
},
// 应用优惠券
applyCoupon(code) {
return this
.setValue('@couponInput', code)
.click('@applyCouponButton');
},
// 等待优惠券结果
waitForCouponResult() {
return this
.waitForElementVisible('@couponSuccess', 3000)
.catch(() => {
// 优惠券可能无效
return this.waitForElementVisible('@couponError', 3000);
});
},
// 点击结账
proceedToCheckout() {
return this.click('@checkoutButton');
},
// 点击继续购物
continueShopping() {
return this.click('@continueShoppingButton');
},
// 清空购物车
clearCart() {
return this.click('@clearCartButton');
},
// 检查购物车是否为空
isEmpty() {
return this.isVisible('@emptyCartMessage');
},
// 等待商品添加成功
waitForItemAdded() {
// 等待通知消息或购物车更新
return this.waitForElementVisible('.notification.success');
}
}]
};
4.1.6 登录测试
// tests/login.test.js
// 登录功能测试套件
describe('登录功能测试', function() {
// 测试配置
before(browser => {
// 设置全局超时
browser.globals.waitForConditionTimeout = 10000;
});
describe('成功登录场景', function() {
it('使用正确的凭据登录成功', function(browser) {
// 打开登录页面
browser
.url('https://shopdemo.com/login')
.waitForElementVisible('input[name="email"]');
// 输入有效凭据
browser
.setValue('input[name="email"]', 'testuser@example.com')
.setValue('input[name="password"]', 'ValidPassword123')
.click('button[type="submit"]');
// 等待跳转到仪表盘
browser.waitForUrlContains('/dashboard');
// 验证登录成功
browser.assert.visible('.welcome-message');
browser.assert.containsText('.user-name', 'Test User');
// 验证 URL 正确
browser.assert.urlEquals('https://shopdemo.com/dashboard');
browser.end();
});
it('勾选记住我后登录', function(browser) {
browser
.url('https://shopdemo.com/login')
.waitForElementVisible('input[name="email"]');
// 勾选记住我
browser
.setValue('input[name="email"]', 'testuser@example.com')
.setValue('input[name="password"]', 'ValidPassword123')
.click('input[name="remember"]')
.click('button[type="submit"]');
// 验证记住我 cookie 被设置
browser.getCookie('remember_token', function(result) {
this.assert.ok(result.value, '记住我令牌已设置');
});
browser.end();
});
});
describe('登录失败场景', function() {
it('使用错误密码登录失败', function(browser) {
browser
.url('https://shopdemo.com/login')
.waitForElementVisible('input[name="email"]');
browser
.setValue('input[name="email"]', 'testuser@example.com')
.setValue('input[name="password"]', 'WrongPassword')
.click('button[type="submit"]');
// 等待错误消息出现
browser.waitForElementVisible('.error-message');
// 验证错误消息
browser.assert.containsText('.error-message', '用户名或密码错误');
// 验证仍然在登录页面
browser.assert.urlContains('/login');
browser.end();
});
it('使用不存在的账号登录失败', function(browser) {
browser
.url('https://shopdemo.com/login')
.waitForElementVisible('input[name="email"]');
browser
.setValue('input[name="email"]', 'nonexistent@example.com')
.setValue('input[name="password"]', 'AnyPassword123')
.click('button[type="submit"]');
browser.waitForElementVisible('.error-message');
browser.assert.containsText('.error-message', '账号不存在');
browser.end();
});
it('使用空邮箱登录失败', function(browser) {
browser
.url('https://shopdemo.com/login')
.waitForElementVisible('input[name="email"]');
browser
.setValue('input[name="email"]', '')
.setValue('input[name="password"]', 'SomePassword')
.click('button[type="submit"]');
// 验证字段验证错误
browser.waitForElementVisible('input[name="email"]:invalid');
browser.end();
});
it('使用无效邮箱格式登录失败', function(browser) {
browser
.url('https://shopdemo.com/login')
.waitForElementVisible('input[name="email"]');
browser
.setValue('input[name="email"]', 'not-an-email')
.click('button[type="submit"]');
browser.waitForElementVisible('.field-error');
browser.assert.containsText('.field-error', '请输入有效的邮箱地址');
browser.end();
});
});
describe('页面链接测试', function() {
it('忘记密码链接跳转正确', function(browser) {
browser
.url('https://shopdemo.com/login')
.waitForElementVisible('input[name="email"]');
browser.click('a[href="/forgot-password"]');
browser.waitForUrlContains('/forgot-password');
browser.assert.visible('input[name="email"]');
browser.assert.containsText('h1', '重置密码');
browser.end();
});
it('注册链接跳转正确', function(browser) {
browser
.url('https://shopdemo.com/login')
.waitForElementVisible('input[name="email"]');
browser.click('a[href="/register"]');
browser.waitForUrlContains('/register');
browser.assert.visible('form.register-form');
browser.end();
});
});
// 清理
after(browser => {
// 清理 session
browser.deleteCookies();
browser.end();
});
});
4.1.7 产品搜索测试
// tests/product-search.test.js
// 产品搜索功能测试
describe('产品搜索功能测试', function() {
before(browser => {
browser.globals.waitForConditionTimeout = 15000;
});
describe('基本搜索功能', function() {
it('搜索关键词显示相关产品', function(browser) {
browser
.url('https://shopdemo.com/products')
.waitForElementVisible('.search-input');
// 执行搜索
browser
.setValue('.search-input', '手机')
.click('.search-button');
// 等待结果
browser.waitForElementVisible('.search-results');
// 验证至少有一个结果
var resultCount;
browser.elements('.product-card', function(result) {
resultCount = result.value.length;
});
browser.perform(function() {
console.log('找到', resultCount, '个结果');
});
// 验证结果包含搜索关键词
browser.elements('.product-name', function(results) {
results.value.forEach(function(item) {
this.getText(item.ELEMENT, function(text) {
console.log('产品:', text.value);
});
}.bind(this));
});
browser.end();
});
it('搜索无结果时显示提示', function(browser) {
browser
.url('https://shopdemo.com/products')
.waitForElementVisible('.search-input');
browser
.setValue('.search-input', 'xyznonexistent123')
.click('.search-button');
// 等待无结果提示
browser.waitForElementVisible('.no-results');
browser.assert.containsText('.no-results', '未找到相关产品');
browser.end();
});
});
describe('分类筛选功能', function() {
it('按分类筛选产品', function(browser) {
browser
.url('https://shopdemo.com/products')
.waitForElementVisible('.category-menu');
// 获取初始产品数量
var initialCount;
browser.elements('.product-card', result => {
initialCount = result.value.length;
});
// 点击第一个分类
browser.click('.category-menu a:first-child');
// 等待产品更新
browser.waitForElementVisible('.product-list');
// 验证分类标题更新
browser.assert.containsText('.category-title', '电子产品');
// 验证产品数量
browser.elements('.product-card', result => {
browser.assert.ok(
result.value.length <= initialCount,
'筛选后产品数量应减少'
);
});
browser.end();
});
it('按价格范围筛选', function(browser) {
browser
.url('https://shopdemo.com/products')
.waitForElementVisible('.filter-section');
// 设置价格范围
browser
.setValue('input[name="min-price"]', '100')
.setValue('input[name="max-price"]', '500')
.click('button.apply-price');
// 等待筛选结果
browser.waitForElementVisible('.product-list');
// 验证所有产品价格都在范围内
browser.elements('.product-price', function(results) {
results.value.forEach(function(item) {
this.getText(item.ELEMENT, function(text) {
// 提取价格数字
var price = parseFloat(text.value.replace(/[^0-9.]/g, ''));
browser.assert.ok(
price >= 100 && price <= 500,
'价格应该在 100-500 范围内'
);
});
}.bind(this));
});
browser.end();
});
});
describe('产品排序功能', function() {
it('按价格从低到高排序', function(browser) {
browser
.url('https://shopdemo.com/products')
.waitForElementVisible('.sort-dropdown');
// 选择价格升序
browser
.click('.sort-dropdown')
.click('option[value="price-asc"]');
// 等待排序完成
browser.waitForElementVisible('.product-list');
browser.pause(500);
// 获取所有产品价格
var prices = [];
browser.elements('.product-price', function(results) {
results.value.forEach(function(item) {
this.getText(item.ELEMENT, function(text) {
prices.push(parseFloat(text.value.replace(/[^0-9.]/g, '')));
});
}.bind(this));
});
// 验证排序(需要等待获取所有价格)
browser.perform(function() {
browser.pause(1000);
// 这里可以添加价格排序验证
});
browser.end();
});
it('按销量排序', function(browser) {
browser
.url('https://shopdemo.com/products')
.waitForElementVisible('.sort-dropdown');
browser
.click('.sort-dropdown')
.click('option[value="sales-desc"]');
browser.waitForElementVisible('.product-list');
// 验证热销标签可见
browser.assert.visible('.bestseller-badge');
browser.end();
});
});
describe('产品卡片交互', function() {
it('悬停产品显示快速操作', function(browser) {
browser
.url('https://shopdemo.com/products')
.waitForElementVisible('.product-card:first-child');
// 悬停到第一个产品
browser.moveToElement('.product-card:first-child');
// 等待快速操作按钮出现
browser.waitForElementVisible('.quick-actions');
browser.assert.visible('.quick-view-button');
browser.assert.visible('.add-to-cart-button');
browser.end();
});
it('点击产品跳转到详情页', function(browser) {
browser
.url('https://shopdemo.com/products')
.waitForElementVisible('.product-card:first-child .product-name');
// 获取产品名称
var productName;
browser.getText('.product-card:first-child .product-name', function(result) {
productName = result.value;
});
// 点击产品卡片
browser.click('.product-card:first-child');
// 等待详情页加载
browser.waitForUrlContains('/product/');
browser.waitForElementVisible('.product-detail');
// 验证产品名称匹配
browser.assert.containsText('.product-title', productName);
browser.end();
});
});
});
4.1.8 购物车功能测试
// tests/cart.test.js
// 购物车功能测试
describe('购物车功能测试', function() {
before(browser => {
// 确保登录状态
browser
.url('https://shopdemo.com/login')
.waitForElementVisible('input[name="email"]')
.setValue('input[name="email"]', 'testuser@example.com')
.setValue('input[name="password"]', 'ValidPassword123')
.click('button[type="submit"]')
.waitForUrlContains('/dashboard');
});
describe('添加到购物车', function() {
it('从产品列表添加商品到购物车', function(browser) {
// 导航到产品列表
browser
.url('https://shopdemo.com/products')
.waitForElementVisible('.product-card:first-child');
// 获取产品名称和价格
var productName, productPrice;
browser.getText('.product-card:first-child .product-name', function(result) {
productName = result.value;
});
browser.getText('.product-card:first-child .product-price', function(result) {
productPrice = result.value;
});
// 点击添加购物车
browser.click('.product-card:first-child .add-to-cart');
// 等待成功提示
browser.waitForElementVisible('.notification.success');
browser.assert.containsText('.notification.success', '已添加到购物车');
// 点击购物车图标查看
browser.click('.cart-icon');
// 验证商品在购物车中
browser.waitForElementVisible('.cart-item');
browser.assert.containsText('.cart-item .item-name', productName);
browser.end();
});
it('添加多件商品', function(browser) {
browser
.url('https://shopdemo.com/products')
.waitForElementVisible('.product-card');
// 添加第一个商品
browser.click('.product-card:nth-child(1) .add-to-cart');
browser.waitForElementVisible('.notification.success');
// 等待通知消失
browser.pause(500);
// 添加第二个商品
browser.click('.product-card:nth-child(2) .add-to-cart');
browser.waitForElementVisible('.notification.success');
// 查看购物车
browser.click('.cart-icon');
browser.waitForElementVisible('.cart-container');
// 验证有两个商品
browser.elements('.cart-item', function(result) {
browser.assert.equal(result.value.length, 2, '购物车应有 2 件商品');
});
browser.end();
});
});
describe('修改购物车数量', function() {
it('增加商品数量', function(browser) {
// 添加商品到购物车
browser
.url('https://shopdemo.com/products')
.waitForElementVisible('.product-card:first-child')
.click('.product-card:first-child .add-to-cart')
.waitForElementVisible('.notification.success')
.click('.cart-icon')
.waitForElementVisible('.cart-container');
// 获取初始数量
browser.getValue('.cart-item:first-child .quantity-input', function(result) {
var initialQty = parseInt(result.value);
// 点击增加按钮
browser.click('.cart-item:first-child .increase-btn');
// 等待更新
browser.pause(500);
// 验证数量增加
browser.getValue('.cart-item:first-child .quantity-input', function(newResult) {
var newQty = parseInt(newResult.value);
browser.assert.equal(newQty, initialQty + 1, '数量应增加 1');
});
});
browser.end();
});
it('减少商品数量', function(browser) {
// 添加并修改数量
browser
.url('https://shopdemo.com/products')
.waitForElementVisible('.product-card:first-child')
.click('.product-card:first-child .add-to-cart')
.waitForElementVisible('.notification.success')
.click('.cart-icon')
.waitForElementVisible('.cart-container');
// 先增加数量
browser.click('.cart-item:first-child .increase-btn');
browser.pause(300);
// 获取当前数量
browser.getValue('.cart-item:first-child .quantity-input', function(result) {
var currentQty = parseInt(result.value);
// 点击减少按钮
browser.click('.cart-item:first-child .decrease-btn');
browser.pause(500);
// 验证数量减少
browser.getValue('.cart-item:first-child .quantity-input', function(newResult) {
var newQty = parseInt(newResult.value);
browser.assert.equal(newQty, currentQty - 1, '数量应减少 1');
});
});
browser.end();
});
it('清空购物车', function(browser) {
// 添加多个商品
browser
.url('https://shopdemo.com/products')
.waitForElementVisible('.product-card:nth-child(1) .add-to-cart')
.click('.product-card:nth-child(1) .add-to-cart')
.waitForElementVisible('.notification.success')
.pause(300)
.click('.product-card:nth-child(2) .add-to-cart')
.waitForElementVisible('.notification.success')
.click('.cart-icon')
.waitForElementVisible('.cart-container');
// 点击清空购物车
browser.click('.clear-cart-btn');
// 确认清空
browser.acceptAlert();
// 等待购物车清空
browser.waitForElementVisible('.empty-cart-message');
browser.assert.containsText('.empty-cart-message', '购物车是空的');
browser.end();
});
});
describe('删除商品', function() {
it('删除单个商品', function(browser) {
// 添加商品
browser
.url('https://shopdemo.com/products')
.waitForElementVisible('.product-card:first-child')
.click('.product-card:first-child .add-to-cart')
.waitForElementVisible('.notification.success')
.click('.cart-icon')
.waitForElementVisible('.cart-container');
// 记录删除前的商品数量
var beforeCount;
browser.elements('.cart-item', result => {
beforeCount = result.value.length;
});
// 点击删除按钮
browser.click('.cart-item:first-child .remove-btn');
// 等待删除动画完成
browser.pause(500);
// 验证商品数量减少
browser.elements('.cart-item', result => {
browser.assert.equal(
result.value.length,
beforeCount - 1,
'商品数量应减少 1'
);
});
browser.end();
});
});
describe('价格计算', function() {
it('验证价格计算正确', function(browser) {
// 添加商品
browser
.url('https://shopdemo.com/products')
.waitForElementVisible('.product-card:first-child')
.click('.product-card:first-child .add-to-cart')
.waitForElementVisible('.notification.success')
.click('.cart-icon')
.waitForElementVisible('.cart-container');
// 获取商品价格和数量
var itemPrice;
browser.getText('.cart-item:first-child .item-price', function(result) {
itemPrice = parseFloat(result.value.replace(/[^0-9.]/g, ''));
});
browser.getValue('.cart-item:first-child .quantity-input', function(result) {
var quantity = parseInt(result.value);
// 计算预期小计
var expectedSubtotal = itemPrice * quantity;
// 获取实际小计
browser.getText('.cart-subtotal', function(subtotalResult) {
var actualSubtotal = parseFloat(
subtotalResult.value.replace(/[^0-9.]/g, '')
);
browser.assert.equal(
actualSubtotal,
expectedSubtotal,
'小计计算应该正确'
);
});
});
browser.end();
});
it('验证优惠券应用', function(browser) {
// 添加商品到购物车
browser
.url('https://shopdemo.com/products')
.waitForElementVisible('.product-card:first-child')
.click('.product-card:first-child .add-to-cart')
.waitForElementVisible('.notification.success')
.click('.cart-icon')
.waitForElementVisible('.cart-container');
// 获取原始总价
var originalTotal;
browser.getText('.cart-total', function(result) {
originalTotal = parseFloat(result.value.replace(/[^0-9.]/g, ''));
});
// 输入优惠券
browser
.setValue('input[name="coupon"]', 'DISCOUNT10')
.click('.apply-coupon-btn');
// 等待优惠券应用
browser.waitForElementVisible('.coupon-success');
// 获取新总价
browser.getText('.cart-total', function(result) {
var newTotal = parseFloat(result.value.replace(/[^0-9.]/g, ''));
// 验证总价降低(假设 10% 折扣)
browser.assert.ok(
newTotal < originalTotal,
'使用优惠券后总价应降低'
);
// 验证折扣金额显示
browser.assert.visible('.discount-amount');
});
browser.end();
});
});
describe('结账流程', function() {
it('导航到结账页面', function(browser) {
// 添加商品
browser
.url('https://shopdemo.com/products')
.waitForElementVisible('.product-card:first-child')
.click('.product-card:first-child .add-to-cart')
.waitForElementVisible('.notification.success')
.click('.cart-icon')
.waitForElementVisible('.cart-container');
// 点击结账按钮
browser.click('.checkout-btn');
// 验证跳转到结账页面
browser.waitForUrlContains('/checkout');
browser.waitForElementVisible('.checkout-form');
// 验证购物车商品信息
browser.assert.visible('.order-summary');
browser.assert.visible('.shipping-form');
browser.end();
});
});
after(browser => {
browser.deleteCookies().end();
});
});
五、常见使用场景
5.1 API 响应验证
// tests/api.test.js
// API 响应验证测试
describe('API 响应验证', function() {
it('验证 API 返回正确的 JSON 数据', function(browser) {
browser
.url('https://api.example.com/data')
.perform(function(done) {
// 使用 execute 执行自定义 JavaScript
this.execute(function() {
return fetch('https://api.example.com/products')
.then(response => response.json())
.then(data => {
// 在 DOM 中存储数据以便验证
window.apiData = data;
return data;
});
}, [], function(result) {
// 验证返回的数据
var data = result.value;
console.log('API 返回数据:', JSON.stringify(data));
// 验证数据结构
this.assert.ok(Array.isArray(data), '返回数据应该是数组');
this.assert.ok(data.length > 0, '数组不应为空');
// 验证第一个元素的结构
var firstItem = data[0];
this.assert.ok(firstItem.id, '应有 id 字段');
this.assert.ok(firstItem.name, '应有 name 字段');
this.assert.ok(firstItem.price, '应有 price 字段');
done();
}.bind(this));
});
browser.end();
});
it('验证 API 错误处理', function(browser) {
browser
.perform(function(done) {
this.execute(function() {
return fetch('https://api.example.com/invalid-endpoint')
.then(response => {
if (!response.ok) {
throw new Error('API 请求失败');
}
return response.json();
})
.catch(error => {
return { error: error.message };
});
}, [], function(result) {
var data = result.value;
// 验证错误被正确处理
this.assert.ok(data.error, '应包含错误信息');
done();
}.bind(this));
});
browser.end();
});
});
5.2 表单验证测试
// tests/form-validation.test.js
// 表单验证测试
describe('表单验证测试', function() {
describe('注册表单验证', function() {
it('验证所有必填字段', function(browser) {
browser
.url('https://shopdemo.com/register')
.waitForElementVisible('form')
.click('button[type="submit"]');
// 验证所有错误提示
browser.assert.visible('input[name="username"] + .error');
browser.assert.containsText('input[name="username"] + .error', '用户名不能为空');
browser.assert.visible('input[name="email"] + .error');
browser.assert.containsText('input[name="email"] + .error', '邮箱不能为空');
browser.assert.visible('input[name="password"] + .error');
browser.assert.containsText('input[name="password"] + .error', '密码不能为空');
browser.end();
});
it('验证邮箱格式', function(browser) {
browser
.url('https://shopdemo.com/register')
.waitForElementVisible('input[name="email"]')
.setValue('input[name="email"]', 'invalid-email')
.click('button[type="submit"]');
browser.waitForElementVisible('input[name="email"] + .error');
browser.assert.containsText(
'input[name="email"] + .error',
'请输入有效的邮箱地址'
);
browser.end();
});
it('验证密码强度', function(browser) {
browser
.url('https://shopdemo.com/register')
.waitForElementVisible('input[name="password"]')
.setValue('input[name="password"]', 'weak');
// 等待密码强度指示器更新
browser.pause(500);
// 验证弱密码提示
browser.assert.containsText('.password-strength', '密码强度:弱');
// 尝试强密码
browser
.clearValue('input[name="password"]')
.setValue('input[name="password"]', 'StrongPass123!');
browser.pause(500);
browser.assert.containsText('.password-strength', '密码强度:强');
browser.end();
});
it('验证密码确认匹配', function(browser) {
browser
.url('https://shopdemo.com/register')
.waitForElementVisible('form')
.setValue('input[name="password"]', 'Password123')
.setValue('input[name="confirm_password"]', 'DifferentPass123')
.click('button[type="submit"]');
browser.waitForElementVisible('input[name="confirm_password"] + .error');
browser.assert.containsText(
'input[name="confirm_password"] + .error',
'两次密码输入不一致'
);
browser.end();
});
});
describe('联系表单测试', function() {
it('提交有效的联系表单', function(browser) {
browser
.url('https://shopdemo.com/contact')
.waitForElementVisible('form.contact-form')
.setValue('input[name="name"]', '张三')
.setValue('input[name="email"]', 'zhangsan@example.com')
.setValue('input[name="phone"]', '13812345678')
.setValue('textarea[name="message"]', '这是一条测试留言')
.click('button[type="submit"]');
// 验证成功消息
browser.waitForElementVisible('.success-message');
browser.assert.containsText('.success-message', '感谢您的留言');
browser.end();
});
});
});
5.3 响应式设计测试
// tests/responsive.test.js
// 响应式设计测试
describe('响应式设计测试', function() {
const viewports = [
{ width: 320, height: 568, name: 'iPhone SE' },
{ width: 375, height: 667, name: 'iPhone X' },
{ width: 768, height: 1024, name: 'iPad' },
{ width: 1280, height: 800, name: '笔记本' },
{ width: 1920, height: 1080, name: '桌面端' }
];
viewports.forEach(viewport => {
it(`在 ${viewport.name} 视口下布局正确`, function(browser) {
// 设置视口大小
browser
.url('https://shopdemo.com')
.resizeWindow(viewport.width, viewport.height);
// 验证导航菜单
if (viewport.width < 768) {
// 移动端应该显示汉堡菜单
browser.assert.visible('.mobile-menu-toggle');
browser.assert.hidden('.desktop-nav');
} else {
// 桌面端应该显示完整导航
browser.assert.hidden('.mobile-menu-toggle');
browser.assert.visible('.desktop-nav');
}
// 验证产品网格
browser.url('https://shopdemo.com/products');
if (viewport.width < 480) {
// 超小屏幕:单列
browser.assert.cssProperty('.product-grid', 'grid-template-columns', '1fr');
} else if (viewport.width < 768) {
// 小屏幕:两列
browser.assert.cssProperty('.product-grid', 'grid-template-columns', '1fr 1fr');
} else if (viewport.width < 1024) {
// 中等屏幕:三列
browser.assert.cssProperty('.product-grid', 'grid-template-columns', '1fr 1fr 1fr');
} else {
// 大屏幕:四列
browser.assert.cssProperty('.product-grid', 'grid-template-columns', 'repeat(4, 1fr)');
}
// 验证字体大小
var baseFontSize;
browser.execute(function() {
return parseFloat(getComputedStyle(document.body).fontSize);
}, [], function(result) {
baseFontSize = result.value;
console.log(`视口 ${viewport.name} 基础字体大小: ${baseFontSize}px`);
});
browser.end();
});
});
});
5.4 文件上传测试
// tests/file-upload.test.js
// 文件上传测试
describe('文件上传测试', function() {
it('上传图片文件', function(browser) {
browser
.url('https://shopdemo.com/upload')
.waitForElementVisible('input[type="file"]');
// 使用 setValue 设置文件路径
browser.setValue(
'input[type="file"]',
require('path').resolve(__dirname, 'fixtures/test-image.jpg')
);
// 点击上传按钮
browser.click('.upload-button');
// 等待上传完成
browser.waitForElementVisible('.upload-success');
browser.assert.containsText('.upload-success', '上传成功');
// 验证预览显示
browser.assert.visible('.image-preview img');
browser.end();
});
it('验证文件类型限制', function(browser) {
browser
.url('https://shopdemo.com/upload')
.waitForElementVisible('input[type="file"]');
// 尝试上传不允许的文件类型
browser.setValue(
'input[type="file"]',
require('path').resolve(__dirname, 'fixtures/document.pdf')
);
browser.click('.upload-button');
// 验证错误提示
browser.waitForElementVisible('.file-error');
browser.assert.containsText('.file-error', '只允许上传图片文件');
browser.end();
});
it('验证文件大小限制', function(browser) {
browser
.url('https://shopdemo.com/upload')
.waitForElementVisible('input[type="file"]');
// 尝试上传超大文件
browser.setValue(
'input[type="file"]',
require('path').resolve(__dirname, 'fixtures/large-image.jpg')
);
browser.click('.upload-button');
// 验证错误提示
browser.waitForElementVisible('.file-error');
browser.assert.containsText('.file-error', '文件大小不能超过 5MB');
browser.end();
});
});
5.5 认证和授权测试
// tests/auth.test.js
// 认证授权测试
describe('认证授权测试', function() {
describe('未登录用户访问受保护页面', function() {
it('应重定向到登录页', function(browser) {
browser
.url('https://shopdemo.com/dashboard')
.waitForUrlContains('/login');
browser.assert.urlContains('/login');
browser.assert.visible('form.login-form');
browser.end();
});
it('应提示需要登录', function(browser) {
browser
.url('https://shopdemo.com/dashboard');
// 等待重定向
browser.pause(1000);
browser.assert.containsText('.page-content', '请先登录');
browser.end();
});
});
describe('登录状态持久化', function() {
it('刷新页面后保持登录状态', function(browser) {
// 登录
browser
.url('https://shopdemo.com/login')
.waitForElementVisible('input[name="email"]')
.setValue('input[name="email"]', 'testuser@example.com')
.setValue('input[name="password"]', 'ValidPassword123')
.click('button[type="submit"]')
.waitForUrlContains('/dashboard');
// 验证已登录
browser.assert.visible('.welcome-message');
// 获取 session cookie
var sessionCookie;
browser.getCookie('session_id', function(result) {
sessionCookie = result.value;
this.assert.ok(sessionCookie, 'Session cookie 应该存在');
}.bind(browser));
// 刷新页面
browser.refresh();
// 验证仍然登录
browser.waitForElementVisible('.welcome-message');
browser.assert.visible('.welcome-message');
browser.end();
});
});
describe('角色权限测试', function() {
it('普通用户不能访问管理后台', function(browser) {
// 以普通用户登录
browser
.url('https://shopdemo.com/login')
.waitForElementVisible('input[name="email"]')
.setValue('input[name="email"]', 'user@example.com')
.setValue('input[name="password"]', 'UserPassword123')
.click('button[type="submit"]');
// 尝试访问管理后台
browser
.url('https://shopdemo.com/admin')
.pause(1000);
// 验证被拒绝
browser.assert.urlContains('/access-denied');
browser.assert.containsText('.error-message', '权限不足');
browser.end();
});
it('管理员可以访问管理后台', function(browser) {
// 以管理员登录
browser
.url('https://shopdemo.com/login')
.waitForElementVisible('input[name="email"]')
.setValue('input[name="email"]', 'admin@example.com')
.setValue('input[name="password"]', 'AdminPassword123')
.click('button[type="submit"]');
// 访问管理后台
browser
.url('https://shopdemo.com/admin')
.waitForElementVisible('.admin-dashboard');
browser.assert.visible('.admin-panel');
browser.assert.visible('.user-management');
browser.end();
});
});
describe('会话过期测试', function() {
it('会话过期后应重新登录', function(browser) {
// 手动清除 session
browser.deleteCookies();
// 尝试访问需要认证的页面
browser
.url('https://shopdemo.com/dashboard')
.waitForUrlContains('/login');
// 验证需要重新登录
browser.assert.visible('form.login-form');
browser.assert.containsText('.page-content', '会话已过期');
browser.end();
});
});
});
六、最佳实践与技巧
6.1 测试组织结构
e2e-tests/
├── nightwatch.conf.js # 配置文件
├── globals.js # 全局变量和钩子
├── commands/ # 自定义命令
│ ├── loginAsAdmin.js
│ ├── waitForAjax.js
│ └── screenshotOnFail.js
├── assertions/ # 自定义断言
│ ├── hasClass.js
│ └── isInViewport.js
├── page-objects/ # 页面对象
│ ├── BasePage.js
│ ├── login/
│ │ └── LoginPage.js
│ ├── dashboard/
│ │ └── DashboardPage.js
│ └── products/
│ ├── ProductListPage.js
│ └── ProductDetailPage.js
├── tests/ # 测试文件
│ ├── smoke/ # 冒烟测试
│ │ └── basic-navigation.js
│ ├── login/ # 登录功能
│ │ ├── login-success.js
│ │ └── login-failure.js
│ ├── products/ # 产品功能
│ │ ├── search.js
│ │ ├── filter.js
│ │ └── detail.js
│ └── regression/ # 回归测试
│ └── checkout-flow.js
├── reports/ # 测试报告
│ ├── screenshots/
│ └── html-reports/
├── fixtures/ # 测试数据
│ ├── users.json
│ ├── products.json
│ └── images/
└── docker/ # Docker 配置
└── docker-compose.yml
6.2 自定义命令
// commands/waitForAjax.js
// 等待 AJAX 请求完成的自定义命令
/**
* 等待所有 AJAX 请求完成
* @param {number} timeout - 超时时间(毫秒)
* @param {function} callback - 回调函数
*/
exports.command = function(timeout = 10000, callback) {
const self = this;
// jQuery 的 active 计数器表示正在进行的 AJAX 请求
function checkAjaxComplete() {
self.execute(function() {
// 尝试多种方式检测 AJAX 状态
if (typeof jQuery !== 'undefined') {
return jQuery.active === 0;
} else if (typeof window.axios !== 'undefined') {
return window.axios.isCancel() || true;
} else {
// 使用全局计数器
return window.ajaxComplete === true;
}
}, [], function(result) {
if (result.value) {
if (callback) {
callback.call(self);
} else {
self.emit('complete');
}
} else {
// 继续等待
setTimeout(checkAjaxComplete, 100);
}
});
}
// 启动超时检查
self.perform(function(done) {
var startTime = Date.now();
function checkWithTimeout() {
self.execute(function() {
if (typeof jQuery !== 'undefined') {
return jQuery.active === 0;
} else {
return window.ajaxComplete === true;
}
}, [], function(result) {
if (result.value) {
done();
} else if (Date.now() - startTime > timeout) {
done(new Error('AJAX 请求超时'));
} else {
setTimeout(checkWithTimeout, 100);
}
});
}
checkWithTimeout();
});
return this;
};
// commands/takeScreenshot.js
// 截取屏幕截图的自定义命令
/**
* 截取屏幕截图
* @param {string} name - 截图名称
*/
exports.command = function(name) {
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
const filename = `${name}-${timestamp}.png`;
return this.saveScreenshot(`reports/screenshots/${filename}`, function() {
console.log(`截图已保存: ${filename}`);
});
};
// commands/loginAs.js
// 登录用户的自定义命令
/**
* 使用指定用户登录
* @param {string} username - 用户名
* @param {string} password - 密码
*/
exports.command = function(username, password) {
return this
.url('/login')
.waitForElementVisible('input[name="email"]')
.setValue('input[name="email"]', username)
.setValue('input[name="password"]', password)
.click('button[type="submit"]')
.waitForUrlContains('/dashboard');
};
6.3 自定义断言
// assertions/hasClass.js
// 验证元素具有指定类名的自定义断言
/**
* 断言元素具有指定类名
* @param {string} selector - 元素选择器
* @param {string} className - 类名
*/
exports.assertion = function(selector, className) {
// 缓存的值
this.expected = className;
this.message = `元素 ${selector} 应包含类名 ${className}`;
// 执行断言
this.pass = function(value) {
return value.includes(className);
};
this.value = function(result) {
return result;
};
this.command = function(callback) {
const self = this;
this.api.execute(function(selector) {
const element = document.querySelector(selector);
if (element) {
return element.className;
}
return null;
}, [selector], function(result) {
callback.call(self, result.value);
});
return this;
};
};
// assertions/isInViewport.js
// 验证元素在视口内的自定义断言
exports.assertion = function(selector) {
this.message = `元素 ${selector} 应在视口内`;
this.pass = function(value) {
return value === true;
};
this.value = function(result) {
return result.value;
};
this.command = function(callback) {
this.api.execute(function(selector) {
const element = document.querySelector(selector);
if (!element) return false;
const rect = element.getBoundingClientRect();
return (
rect.top >= 0 &&
rect.left >= 0 &&
rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) &&
rect.right <= (window.innerWidth || document.documentElement.clientWidth)
);
}, [selector], function(result) {
callback.call(this, result.value);
});
return this;
};
};
6.4 全局配置
// globals.js
// 全局配置和钩子
module.exports = {
// 全局测试超时
waitForConditionTimeout: 10000,
waitForConditionPollInterval: 100,
// 全局钩子
before: function(browser) {
console.log('========================================');
console.log('测试开始 - 初始化环境');
console.log('========================================');
// 设置全局等待时间
browser.globals.waitForConditionTimeout = 10000;
// 设置测试环境
browser.globals.testEnvironment = process.env.TEST_ENV || 'staging';
// 初始化测试数据
browser.globals.testUsers = {
admin: {
email: 'admin@test.com',
password: 'AdminPass123'
},
user: {
email: 'user@test.com',
password: 'UserPass123'
}
};
},
after: function(browser) {
console.log('========================================');
console.log('测试完成 - 清理环境');
console.log('========================================');
browser.end();
},
// 每个测试套件前执行
beforeEach: function(browser) {
console.log(`开始测试: ${browser.currentTest.name}`);
// 重置 cookies
browser.deleteCookies();
// 设置默认视口
browser.resizeWindow(1280, 800);
},
// 每个测试套件后执行
afterEach: function(browser, done) {
console.log(`完成测试: ${browser.currentTest.name}`);
console.log(`结果: ${browser.currentTest.results.passed ? '通过' : '失败'}`);
// 如果测试失败且需要截图
if (!browser.currentTest.results.passed && browser.currentTest.results.errors.length > 0) {
const testName = browser.currentTest.name.replace(/[^a-z0-9]/gi, '_');
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
browser.saveScreenshot(
`reports/screenshots/failure_${testName}_${timestamp}.png`,
function() {
done();
}
);
} else {
done();
}
}
};
6.5 并行测试配置
// nightwatch.conf.js (并行测试配置)
module.exports = {
src_folders: ['tests'],
output_folder: 'reports',
// 启用并行测试
parallelism: {
// 每个 worker 的超时时间
timeout: 300000,
// 重试次数
retries: 2,
// Worker 数量(默认使用 CPU 核心数)
workers: 'auto'
},
webdriver: {
start_process: true,
port: 9515,
server_path: '',
cli_args: [
'--no-sandbox',
'--disable-setuid-sandbox'
]
},
test_settings: {
default: {
launch_url: 'https://shopdemo.com',
screenshots: {
enabled: true,
on_failure: true,
path: 'reports/screenshots'
},
desiredCapabilities: {
browserName: 'chrome',
'goog:chromeOptions': {
args: [
'--headless',
'--no-sandbox',
'--disable-gpu',
'--window-size=1280,800',
'--disable-dev-shm-usage'
]
}
}
}
}
};
6.6 CI/CD 集成
GitHub Actions 配置
# .github/workflows/e2e-tests.yml
name: E2E Tests
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
jobs:
e2e-tests:
runs-on: ubuntu-latest
# ============================================
# 服务容器配置
# ============================================
services:
chrome:
image: selenium/standalone-chrome:latest
ports:
- 4444:4444
app:
image: node:18
env:
NODE_ENV: test
APP_URL: https://shopdemo.com
ports:
- 3000:3000
options: >-
--health-cmd "curl -f http://localhost:3000/health"
--health-interval 30s
--health-timeout 10s
--health-retries 5
steps:
# ============================================
# 检出代码
# ============================================
- name: Checkout code
uses: actions/checkout@v3
# ============================================
# 设置 Node.js 环境
# ============================================
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '18'
cache: 'npm'
# ============================================
# 安装依赖
# ============================================
- name: Install dependencies
run: npm ci
# ============================================
# 运行测试
# ============================================
- name: Run E2E tests
run: npm test
env:
CHROME_PORT: 4444
APP_URL: https://shopdemo.com
# ============================================
# 上传测试报告
# ============================================
- name: Upload test results
if: always()
uses: actions/upload-artifact@v3
with:
name: test-reports
path: reports/
# ============================================
# 上传失败截图
# ============================================
- name: Upload failure screenshots
if: failure()
uses: actions/upload-artifact@v3
with:
name: failure-screenshots
path: reports/screenshots/*.png
6.7 测试数据管理
// fixtures/users.json
{
"validUsers": [
{
"id": "user-001",
"email": "admin@test.com",
"password": "AdminPass123!",
"role": "admin",
"name": "管理员"
},
{
"id": "user-002",
"email": "test@example.com",
"password": "TestPass123!",
"role": "user",
"name": "测试用户"
}
],
"invalidUsers": [
{
"email": "invalid-email",
"password": "short",
"expectedError": "邮箱格式不正确"
},
{
"email": "nonexistent@test.com",
"password": "WrongPassword",
"expectedError": "用户不存在"
}
]
}
// tests/data-driven.test.js
// 数据驱动测试示例
const users = require('../fixtures/users.json');
describe('数据驱动登录测试', function() {
users.validUsers.forEach(user => {
it(`使用 ${user.name} 登录应该成功`, function(browser) {
browser
.url('https://shopdemo.com/login')
.waitForElementVisible('input[name="email"]')
.setValue('input[name="email"]', user.email)
.setValue('input[name="password"]', user.password)
.click('button[type="submit"]')
.waitForUrlContains('/dashboard');
// 验证用户信息
browser.assert.containsText('.user-name', user.name);
browser.assert.visible(`.role-badge.${user.role}`);
browser.end();
});
});
users.invalidUsers.forEach(user => {
it(`验证错误: ${user.expectedError}`, function(browser) {
browser
.url('https://shopdemo.com/login')
.waitForElementVisible('input[name="email"]')
.setValue('input[name="email"]', user.email)
.setValue('input[name="password"]', user.password)
.click('button[type="submit"]')
.waitForElementVisible('.error-message');
browser.assert.containsText('.error-message', user.expectedError);
browser.end();
});
});
});
6.8 调试技巧
// 调试模式下的测试示例
describe('调试测试', function() {
it('带详细日志的测试', function(browser) {
browser
.url('https://shopdemo.com')
.perform(function(done) {
console.log('===== 开始调试 =====');
console.log('当前 URL:', this.launchUrl);
// 获取页面信息
this.execute(function() {
return {
title: document.title,
url: window.location.href,
readyState: document.readyState
};
}, [], function(result) {
console.log('页面标题:', result.value.title);
console.log('页面状态:', result.value.readyState);
});
done();
})
.pause(5000) // 暂停以便观察
.end();
});
it('使用 debug 命令', function(browser) {
browser
.url('https://shopdemo.com')
.waitForElementVisible('body');
// 在这里设置断点
// 可以打开 DevTools 检查浏览器状态
browser.debug(); // 暂停执行进入调试模式
// 继续执行
browser.end();
});
});
6.9 性能测试
// tests/performance.test.js
// 性能测试
describe('性能测试', function() {
it('测量页面加载时间', function(browser) {
const startTime = Date.now();
browser.url('https://shopdemo.com');
browser.perform(function() {
const endTime = Date.now();
const loadTime = endTime - startTime;
console.log(`页面加载时间: ${loadTime}ms`);
// 断言加载时间
browser.assert.ok(
loadTime < 3000,
`页面加载时间应小于 3 秒,实际: ${loadTime}ms`
);
});
browser.end();
});
it('测量 API 响应时间', function(browser) {
const startTime = Date.now();
browser.perform(function(done) {
this.execute(function() {
const start = Date.now();
return fetch('/api/products')
.then(response => {
const end = Date.now();
return {
status: response.status,
time: end - start
};
});
}, [], function(result) {
console.log(`API 响应时间: ${result.value.time}ms`);
this.assert.ok(
result.value.time < 1000,
`API 响应时间应小于 1 秒,实际: ${result.value.time}ms`
);
this.assert.equal(result.value.status, 200);
done();
}.bind(this));
});
browser.end();
});
it('测量关键元素渲染时间', function(browser) {
browser.url('https://shopdemo.com');
const startTime = Date.now();
browser.waitForElementVisible('.main-content', function() {
const renderTime = Date.now() - startTime;
console.log(`主要内容渲染时间: ${renderTime}ms`);
browser.assert.ok(
renderTime < 2000,
`渲染时间应小于 2 秒,实际: ${renderTime}ms`
);
});
browser.end();
});
});
七、总结与资源
7.1 Nightwatch.js 核心要点回顾
Nightwatch.js 学习路径:
│
├── 基础入门
│ ├── 安装和配置
│ ├── 编写第一个测试
│ └── 运行和调试测试
│
├── 核心功能
│ ├── 元素定位(CSS/XPath)
│ ├── 浏览器操作(导航、窗口、iframe)
│ ├── 交互操作(点击、输入、拖拽)
│ ├── 断言系统(assert/expect)
│ └── 等待机制(显式/隐式)
│
├── 进阶特性
│ ├── Page Object 模式
│ ├── 自定义命令和断言
│ ├── 全局配置和钩子
│ └── 测试数据管理
│
├── 高级应用
│ ├── 并行测试执行
│ ├── CI/CD 集成
│ ├── 响应式测试
│ └── 性能测试
│
└── 最佳实践
├── 测试组织结构
├── 代码复用
├── 维护性优化
└── 持续改进
7.2 官方资源链接
官方文档和资源:
├── Nightwatch.js 官方文档: https://nightwatchjs.org/
├── GitHub 仓库: https://github.com/nightwatchjs/nightwatch
├── API 参考: https://nightwatchjs.org/api/
├── 示例项目: https://github.com/nightwatchjs/nightwatch/tree/master/examples
└── 社区论坛: https://discuss.nightwatchjs.org/
7.3 相关推荐项目
测试工具生态:
├── Selenium WebDriver - WebDriver 协议实现
├── Playwright - 微软出品的端到端测试框架
├── Cypress - 现代前端测试框架
├── Puppeteer - Chrome/Chromium 控制库
├── WebdriverIO - JavaScript 的 WebDriver 封装
├── TestCafe - 无需 WebDriver 的测试框架
└── Jest + Testing Library - React 组件测试
7.4 继续学习路径
成为 Nightwatch 专家的学习路径:
│
├── 第一阶段:基础掌握(1-2 周)
│ ├── 完成官方入门教程
│ ├── 编写 10+ 个基础测试用例
│ └── 掌握元素定位和断言
│
├── 第二阶段:实践应用(2-4 周)
│ ├── 在实际项目中应用
│ ├── 掌握 Page Object 模式
│ └── 实现完整的测试套件
│
├── 第三阶段:高级特性(1-2 月)
│ ├── 自定义命令和断言开发
│ ├── CI/CD 集成实践
│ └── 测试报告优化
│
└── 第四阶段:团队贡献(持续)
├── 测试框架贡献
├── 最佳实践分享
└── 团队培训指导
7.5 结语
通过这篇完整的教程,你已经学习了 Nightwatch.js 的各个方面,从基础的安装配置到高级的 CI/CD 集成。Nightwatch.js 凭借其简洁的 API、强大的功能和优秀的可扩展性,成为了端到端测试领域的优秀选择。
记住,优秀的测试不是一蹴而就的,需要在实践中不断优化和完善。建议你:
实践建议:
├── 从简单的冒烟测试开始
├── 逐步覆盖核心业务流程
├── 保持测试代码的整洁和可维护
├── 持续关注测试执行时间和稳定性
├── 定期回顾和改进测试策略
└── 与团队分享测试最佳实践
现在,是时候开始你的 Nightwatch.js 之旅了。祝你测试愉快!
相关文章推荐:
评论区