网页老出bug怎么查web应用测试入门教程从登录注册到支付下单新手一步步学会
先说个大实话,很多做web应用的朋友,尤其是刚入行或者自己折腾小项目的,遇到bug就是抓狂。今天这个bug修好了,明天又冒出来一个,越改越乱,心态直接崩掉。
我见过太多人在bug面前一脸茫然的样子了,别慌,今天咱们从头到尾捋一捋,从最基础的登录注册到最让人头疼的支付下单,教你一套完整的方法,保证你能看懂、能上手、能落地。
一、先搞明白,bug到底长什么样
在开始排查之前,你至少得知道bug的几种常见形态,不然你连自己遇到的是啥都不清楚。
界面层面的bug
比如页面显示错乱,按钮点了没反应,文字被截断,图片加载不出来。这类bug最直观,有时候截图往群里一发,别人一眼就能看出来。
逻辑层面的bug
这个就比较隐蔽了,比如用户注册了但收不到验证码,提交表单后数据没保存,或者支付成功了但订单状态还是待付款。这类bug最让人头疼,因为界面看起来没问题,但实际功能就是跑不通。
性能层面的bug
页面加载特别慢,数据量大时直接卡死,并发请求一多就超时或者报错。这类bug在高流量场景下特别容易暴露。
安全层面的bug
这个最要命,比如越权访问、SQL注入、XSS攻击等,轻则数据泄露,重则整个应用被黑。
二、登录注册模块:最容易踩坑的地方
先说登录功能
登录是最基础的功能,也是最容易出问题的地方。咱们一步步来。
1. 正常流程测试
先跑一遍正常流程,用正确的账号密码登录,看看能不能进系统,登录后页面跳转对不对,有没有跳到预期的页面。
// 用自动化测试脚本模拟登录流程
const puppeteer = require('puppeteer');
(async () => {
const browser = await puppeteer.launch();
const page = await browser.newPage();
// 打开登录页
await page.goto('https://yourwebsite.com/login');
// 填写用户名
await page.type('#username', 'testuser');
// 填写密码
await page.type('#password', 'Test1234!');
// 点击登录按钮
await page.click('#login-btn');
// 等待跳转,检查是否登录成功
await page.waitForNavigation();
// 检查URL是否包含预期的登录后页面
const currentUrl = page.url();
console.log('登录后跳转到了:', currentUrl);
// 检查页面上是否有用户信息展示
const userInfo = await page.$eval('.user-info', el => el.textContent);
console.log('用户信息:', userInfo);
await browser.close();
})();
2. 异常场景测试
这才是重点,很多人只管正常流程,结果异常场景全炸了。
- 空账号密码登录:看看有没有提示,提示信息明不明显
- 错误密码登录:提示要友好,别直接显示”密码错误”这种,容易被破解者利用
- 账号不存在登录:提示要模糊一点,比如”账号或密码错误”
- 连续输错密码:看看有没有锁定机制
- SQL注入测试:输入
' OR '1'='1这类经典payload,看系统能不能挡住
// 测试SQL注入防护
async function testSQLInjection(page) {
await page.goto('https://yourwebsite.com/login');
// 尝试注入用户名
await page.type('#username', "' OR '1'='1");
await page.type('#password', "' OR '1'='1");
await page.click('#login-btn');
// 检查结果,是否跳到了正常页面或者报错
const currentUrl = page.url();
const errorMessage = await page.$eval('.error-msg', el => el.textContent).catch(() => 'none');
console.log('注入测试后URL:', currentUrl);
console.log('错误信息:', errorMessage);
// 如果URL还是登录页,说明防护成功
if (currentUrl.includes('/login')) {
console.log('✓ SQL注入防护成功');
} else {
console.log('✗ 存在SQL注入风险!');
}
}
3. 密码相关的安全测试
- 密码传输是否用了HTTPS
- 密码在数据库中是否加密存储(绝对不能明文存储)
- 有没有密码强度校验
- 有没有密码重置功能,重置链接有没有过期机制
再说注册功能
注册比登录更复杂,因为涉及的数据更多,校验规则也更多。
1. 基本校验
// 前端注册表单校验示例
function validateRegisterForm(data) {
const errors = {};
// 用户名校验
if (!data.username) {
errors.username = '用户名不能为空';
} else if (data.username.length < 3 || data.username.length > 20) {
errors.username = '用户名长度需在3-20个字符之间';
} else if (!/^[a-zA-Z0-9_]+$/.test(data.username)) {
errors.username = '用户名只能包含字母、数字和下划线';
}
// 邮箱校验
if (!data.email) {
errors.email = '邮箱不能为空';
} else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(data.email)) {
errors.email = '邮箱格式不正确';
}
// 密码校验
if (!data.password) {
errors.password = '密码不能为空';
} else if (data.password.length < 8) {
errors.password = '密码长度至少8位';
} else if (!/(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/.test(data.password)) {
errors.password = '密码需包含大小写字母和数字';
}
return errors;
}
2. 注册后的流程校验
- 注册成功后有没有发邮件/短信验证
- 验证链接有没有过期时间
- 同一个邮箱/手机号能不能重复注册
- 注册后账号状态是什么,是立即激活还是需要验证
3. 并发注册测试
// 测试并发注册同一邮箱
async function testConcurrentRegistration(page) {
const promises = [];
for (let i = 0; i < 5; i++) {
const testPage = await page.browser().newPage();
promises.push((async (p, index) => {
await p.goto('https://yourwebsite.com/register');
await p.type('#email', `test${index}@example.com`);
await p.type('#username', `testuser${index}`);
await p.type('#password', 'Test1234!');
await p.click('#register-btn');
})(testPage, i));
}
await Promise.all(promises);
console.log('并发注册测试完成');
}
三、支付下单模块:最不能出问题的地方
支付功能,尤其是涉及金钱的,一出bug就是真金白银的损失。这个模块的测试必须做扎实。
1. 订单流程完整测试
// 完整的下单流程测试
async function testOrderFlow(page) {
// 登录
await page.goto('https://yourwebsite.com/login');
await page.type('#username', 'testuser');
await page.type('#password', 'Test1234!');
await page.click('#login-btn');
await page.waitForNavigation();
// 选择商品加入购物车
await page.goto('https://yourwebsite.com/product/123');
await page.click('#add-to-cart');
await page.waitForSelector('.cart-badge');
// 进入结算页
await page.click('#go-to-checkout');
await page.waitForNavigation();
// 填写收货地址
await page.type('#address', '测试地址123号');
await page.type('#phone', '13800138000');
await page.type('#name', '测试用户');
// 选择支付方式
await page.click('#payment-wechat');
// 点击提交订单
await page.click('#submit-order');
// 检查订单是否创建成功
await page.waitForSelector('.order-success');
const orderInfo = await page.$eval('.order-info', el => el.textContent);
console.log('订单信息:', orderInfo);
}
2. 支付相关的关键测试点
金额计算
这是最核心的,任何一分钱算错都是大问题。
- 商品价格是否正确
- 运费计算对不对
- 优惠券/折扣是否生效
- 税费计算是否准确
- 总金额 = 商品价 + 运费 + 税费 - 优惠,这个等式必须成立
// 金额计算测试
async function testAmountCalculation(page) {
// 添加两个商品
await page.goto('https://yourwebsite.com/product/101');
await page.type('#quantity', '2');
await page.click('#add-to-cart');
await page.goto('https://yourwebsite.com/product/102');
await page.type('#quantity', '1');
await page.click('#add-to-cart');
// 进入结算页查看金额明细
await page.goto('https://yourwebsite.com/checkout');
const product1Price = await page.$eval('.product-1-price', el => parseFloat(el.textContent));
const product2Price = await page.$eval('.product-2-price', el => parseFloat(el.textContent));
const shippingFee = await page.$eval('.shipping-fee', el => parseFloat(el.textContent));
const discount = await page.$eval('.discount', el => parseFloat(el.textContent));
const total = await page.$eval('.total-amount', el => parseFloat(el.textContent));
// 计算预期总额
const expectedTotal = (product1Price * 2) + product2Price + shippingFee - discount;
// 允许0.01的误差(浮点数计算问题)
const diff = Math.abs(total - expectedTotal);
if (diff <= 0.01) {
console.log('✓ 金额计算正确,总额:', total);
} else {
console.log('✗ 金额计算错误! 预期:', expectedTotal, '实际:', total, '误差:', diff);
}
}
优惠/优惠券测试
// 优惠券测试
async function testCoupons(page) {
const coupons = [
{ code: 'SAVE10', type: 'percentage', value: 10, minAmount: 100 },
{ code: 'FLAT20', type: 'fixed', value: 20, minAmount: 50 },
{ code: 'INVALID', type: 'percentage', value: 5, minAmount: 0 } // 无效优惠券
];
for (const coupon of coupons) {
await page.goto('https://yourwebsite.com/checkout');
// 输入优惠券
await page.type('#coupon-code', coupon.code);
await page.click('#apply-coupon');
await page.waitForSelector('.coupon-result');
const result = await page.$eval('.coupon-result', el => el.textContent);
console.log(`优惠券 ${coupon.code}: ${result}`);
}
}
库存测试
- 库存为0时能否下单
- 下单后库存是否正确扣减
- 支付失败后库存是否回滚
- 多人同时买同一件库存,会不会超卖
// 测试超卖问题
async function testOverSelling(browser) {
const pages = [];
const totalOrders = [];
// 开启10个并发请求
for (let i = 0; i < 10; i++) {
const page = await browser.newPage();
pages.push(page);
const orderPromise = (async (p, index) => {
await p.goto('https://yourwebsite.com/product/101');
await p.type('#quantity', '1');
await p.click('#add-to-cart');
await p.click('#go-to-checkout');
// 模拟支付
await p.click('#submit-order');
await p.waitForNavigation();
// 获取订单号
const orderId = await p.$eval('.order-id', el => el.textContent).catch(() => 'failed');
return { userId: index, orderId };
})(p, i);
totalOrders.push(orderPromise);
}
const results = await Promise.all(totalOrders);
const successCount = results.filter(r => r.orderId !== 'failed').length;
console.log(`成功下单: ${successCount} 笔`);
console.log(`详细结果:`, results);
}
支付回调测试
这是最容易出问题的地方之一,支付成功但订单没更新,或者支付失败但订单显示已支付。
// 模拟支付回调
const express = require('express');
const app = express();
// 支付回调接口
app.post('/api/payment/callback', (req, res) => {
const { orderId, payChannel, status, amount } = req.body;
// 1. 验证签名(重要!防止伪造回调)
const signature = req.headers['x-signature'];
if (!verifySignature(req.body, signature)) {
return res.status(403).json({ code: 403, msg: '签名验证失败' });
}
// 2. 幂等性检查(防止重复处理)
const order = db.orders.findById(orderId);
if (order && order.status === 'paid') {
console.log('订单已处理,跳过重复回调');
return res.json({ code: 200, msg: 'success' });
}
// 3. 更新订单状态
if (status === 'success') {
order.status = 'paid';
order.paidAt = new Date();
order.payChannel = payChannel;
order.payAmount = amount;
} else if (status === 'failed') {
order.status = 'payment_failed';
}
db.orders.save(order);
// 4. 发送通知
sendNotification(order.userId, `您的订单${orderId}支付${status === 'success' ? '成功' : '失败'}`);
res.json({ code: 200, msg: 'success' });
});
// 签名验证函数
function verifySignature(data, signature) {
const secret = process.env.PAYMENT_SECRET;
const stringToSign = Object.keys(data).sort().map(k => `${k}=${data[k]}`).join('&');
const calculatedSign = crypto.createHmac('sha256', secret)
.update(stringToSign)
.digest('hex');
return calculatedSign === signature;
}
app.listen(3000, () => console.log('Payment callback server running on port 3000'));
四、测试策略:从手工到自动化
手工测试:最基础但最重要的
手工测试是根本,再好的自动化也替代不了人对细节的感知。
测试 checklist
登录模块
□ 正常登录成功
□ 空账号密码提示
□ 错误密码提示
□ 账号不存在提示
□ SQL注入防护
□ 密码长度校验
□ 登录超时处理
□ 记住登录状态(勾选后下次自动登录)
□ 多设备登录处理
注册模块
□ 正常注册成功
□ 邮箱格式校验
□ 密码强度校验
□ 重复邮箱提示
□ 验证码发送和验证
□ 注册协议勾选
□ 注册后状态
支付下单模块
□ 商品金额计算
□ 运费计算
□ 优惠券使用
□ 支付方式选择
□ 提交订单成功
□ 支付回调处理
□ 库存扣减
□ 支付超时处理
□ 重复支付处理
□ 退款流程
自动化测试:提升效率的神器
当你把手工测试跑通了,就可以考虑用自动化来提高效率了。
用Playwright做E2E测试
// playwright测试示例
const { test, expect } = require('@playwright/test');
test.describe('登录注册测试', () => {
test('正常登录应该成功', async ({ page }) => {
await page.goto('https://yourwebsite.com/login');
await page.fill('#username', 'testuser');
await page.fill('#password', 'Test1234!');
await page.click('#login-btn');
await expect(page).toHaveURL(/.*\/dashboard/);
await expect(page.locator('.user-info')).toBeVisible();
});
test('错误密码应该提示错误', async ({ page }) => {
await page.goto('https://yourwebsite.com/login');
await page.fill('#username', 'testuser');
await page.fill('#password', 'WrongPassword');
await page.click('#login-btn');
await expect(page.locator('.error-msg')).toContainText('账号或密码错误');
});
test('连续输错5次密码应该锁定', async ({ page }) => {
for (let i = 0; i < 5; i++) {
await page.goto('https://yourwebsite.com/login');
await page.fill('#username', 'testuser');
await page.fill('#password', `WrongPassword${i}`);
await page.click('#login-btn');
}
// 第5次后应该被锁定
await expect(page.locator('.lock-msg')).toBeVisible();
});
});
test.describe('支付下单测试', () => {
test('下单后金额计算应该正确', async ({ page }) => {
await page.goto('https://yourwebsite.com/product/101');
await page.fill('#quantity', '2');
await page.click('#add-to-cart');
await page.goto('https://yourwebsite.com/checkout');
const subtotal = await page.$eval('.subtotal', el => parseFloat(el.textContent));
const shipping = await page.$eval('.shipping', el => parseFloat(el.textContent));
const total = await page.$eval('.total', el => parseFloat(el.textContent));
expect(total).toBeCloseTo(subtotal + shipping, 2);
});
test('优惠券应该正确抵扣', async ({ page }) => {
await page.goto('https://yourwebsite.com/checkout');
await page.fill('#coupon-code', 'SAVE10');
await page.click('#apply-coupon');
await expect(page.locator('.discount-amount')).toContainText('10%');
const totalWithDiscount = await page.$eval('.total', el => parseFloat(el.textContent));
expect(totalWithDiscount).toBeLessThan(100); // 假设原价100
});
});
五、调试技巧:快速定位bug
1. 善用浏览器开发者工具
这是最简单也最强大的工具,F12打开,主要看这几个面板:
Network面板
所有网络请求都在这,重点看:
- 请求的URL对不对
- 请求方法对不对(GET/POST)
- 请求参数有没有遗漏
- 响应状态码(200成功,4xx客户端错误,5xx服务端错误)
- 响应时间(有没有慢查询)
// 快速筛选错误请求
在Network面板过滤框输入 "status:5xx" 可以快速定位服务端错误
Console面板
前端报错全在这,红色的是error,黄色的warning。
// 主动打印调试信息
console.log('表单提交,参数:', formData);
console.error('网络请求失败:', error);
console.warn('缓存已过期,需要重新加载');
Sources面板
需要打断点调试的时候用这个,可以在代码执行到特定位置时停下来,查看变量值、调用栈等。
// 设置断点调试
debugger; // 这行代码会让浏览器自动停下来,方便查看当前状态
2. 日志是调试的神器
后端日志一定要记录关键操作,不然出问题就是两眼一抹黑。
// 好的日志记录方式
const logger = {
info: (msg, data = {}) => {
console.log(`[${new Date().toISOString()}] [INFO] ${msg}`, data);
},
error: (msg, error) => {
console.error(`[${new Date().toISOString()}] [ERROR] ${msg}`, error);
},
warn: (msg, data = {}) => {
console.warn(`[${new Date().toISOString()}] [WARN] ${msg}`, data);
}
};
// 使用示例
async function createOrder(userId, items) {
logger.info('开始创建订单', { userId, items });
try {
// 检查库存
const inventoryCheck = await checkInventory(items);
logger.info('库存检查通过', { inventoryCheck });
// 计算金额
const total = calculateTotal(items);
logger.info('金额计算完成', { total });
// 创建订单
const order = await db.orders.create({ userId, items, total });
logger.info('订单创建成功', { orderId: order.id });
return order;
} catch (error) {
logger.error('创建订单失败', { error: error.message, stack: error.stack });
throw error;
}
}
3. 复现是解决问题的第一步
遇到bug先别急着改,先搞清楚:
- 怎么复现的?(具体操作步骤)
- 什么情况下会出现?(特定的数据?特定的浏览器?特定的网络环境?)
- 是必现还是偶现?
- 影响范围有多大?
只有把这些问题搞清楚了,才能精准定位和修复。
六、常见的坑和踩坑经验
1. 前后端时间不一致
这个问题太常见了,前端用浏览器时间,后端用服务器时间,时区还对不上,订单时间、发货时间全乱了。
// 统一用UTC时间处理
const now = new Date().toISOString(); // 永远用这个,不要用本地时间
2. 浮点数计算精度问题
// 错误的金额计算
0.1 + 0.2 === 0.30000000000000004 // true,这就是坑
// 正确的做法:用整数计算或者专用库
const { Decimal } = require('decimal.js');
const result = new Decimal(0.1).plus(0.2).toNumber(); // 0.3
3. 幂等性问题
用户网络抖动,重复点击支付按钮,结果扣了两次钱。
// 用分布式锁防止重复提交
async function processPayment(orderId, userId) {
const lockKey = `payment:${orderId}`;
const locked = await redis.set(lockKey, '1', 'EX', 30, 'NX');
if (!locked) {
throw new Error('请勿重复提交');
}
try {
// 处理支付逻辑
const result = await paymentGateway.charge(orderId, userId);
return result;
} finally {
await redis.del(lockKey);
}
}
4. 接口超时处理
// 设置合理的超时时间
async function fetchProduct(id) {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 5000);
try {
const response = await fetch(`/api/product/${id}`, {
signal: controller.signal
});
return await response.json();
} catch (error) {
if (error.name === 'AbortError') {
throw new Error('请求超时,请稍后重试');
}
throw error;
} finally {
clearTimeout(timeoutId);
}
}
七、给新手的测试建议
1. 从手工测试开始
别一上来就搞自动化,先把手工测试流程跑顺了,知道什么情况该测什么,再考虑自动化。
2. 建立自己的测试用例库
把每次遇到的bug和对应的测试用例记录下来,每次上线前跑一遍,避免同样的问题出现两次。
3. 灰度发布
别一次全量发布,先放一小部分用户,观察一段时间没问题了再全量。
4. 监控和告警
// 简单的性能监控
const monitor = {
track: async (event, data) => {
await fetch('/api/metrics', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ event, data, timestamp: Date.now() })
});
},
checkHealth: async () => {
const checks = [
{ name: 'database', check: checkDatabase },
{ name: 'payment', check: checkPaymentService },
{ name: 'cache', check: checkCache }
];
for (const { name, check } of checks) {
try {
await check();
await this.track('health_ok', { service: name });
} catch (error) {
await this.track('health_error', { service: name, error: error.message });
// 发送告警
sendAlert(`服务 ${name} 健康检查失败: ${error.message}`);
}
}
}
};
5. 不要忽视用户反馈
用户报的bug,哪怕看起来很简单,也要认真排查。很多时候用户发现的问题,正是你测试用例里没覆盖到的场景。
八、常用测试工具和资源
浏览器工具
- Chrome DevTools(必备)
- Postman(接口测试)
- Swagger UI(API文档和测试)
自动化测试框架
- Playwright(新一代,推荐)
- Cypress(前端测试神器)
- Selenium(老牌,生态好)
性能测试
- Apache JMeter
- k6(新兴的性能测试工具)
安全测试
- OWASP ZAP(免费)
- Burp Suite(专业)
最后说几句掏心窝的话,web应用测试这事儿,没有捷径,就是多测、多想、多记录。每一个bug都是帮你把系统变好的机会,别嫌麻烦,该做的测试一步都不能省。
记住,测试不是为了证明程序没bug,而是为了尽可能多地发现bug。保持好奇心,遇到问题多问几个为什么,你很快就从新手变成老手了。
