项目上线前,到底要做哪些测试?
一、盒模型:白盒、黑盒、灰盒
这是软件测试领域最基础的分类。把软件想象成一个盒子,测试者能否看到盒子内部来区分。
白盒测试
盒子透明,能看到内部。测试者基于源码逻辑设计测试用例。
能发现的问题:
- 递归无深度限制导致栈溢出
- 内存泄漏(如 Map 未清理过期记录)
- 代码重复定义
- 缺少数据库唯一约束
- React hydration mismatch
这些问题从外部访问网站时表现正常,但代码本身有隐患。典型手段是代码审查(Code Review)和静态分析工具。
黑盒测试
盒子不透明,只看外部行为。测试者不看源码,只从外部输入并观察输出。
能发现的问题:
- 运行时时序漏洞(如认证检查在数据查询之后执行,redirect 前数据已泄露)
- 基础设施配置缺陷(如镜像仓库允许匿名拉取、Nginx 缺少 Host 白名单)
- Git 历史中的敏感信息泄露
- HTTP 头注入等协议层攻击
这些问题读代码很难发现,因为代码逻辑本身可能没错,问题出在运行时行为或部署环境。典型手段是渗透测试。
灰盒测试
半透明。部分了解内部结构(如知道技术栈和架构但不见全部代码),结合内外信息设计测试。
实际项目中,纯粹的黑白盒都少见。渗透测试者通常会先做信息收集(OSINT)了解目标技术栈,再结合公开文档推测内部实现,这就是灰盒。
白盒与黑盒的关系
互补,不替代。
| 维度 | 白盒 | 黑盒 |
|---|---|---|
| 视角 | 开发者视角 | 攻击者视角 |
| 能发现 | 代码健壮性、编码规范、逻辑缺陷 | 运行时漏洞、配置缺陷、基础设施问题 |
| 不能发现 | 运行时时序问题、部署环境问题 | 代码内部隐患、性能瓶颈根因 |
| 成本 | 需要读懂源码,人力成本高 | 需要攻击经验和工具,技能门槛高 |
| 时机 | 开发阶段持续进行 | 上线前集中进行 |
二、按测试目标划分
单元测试(Unit Test)
测试单个函数或模块的正确性。是最小粒度的测试。
// 示例:测试一个 slug 生成函数
test('slugifyPostTitle 应将中文标题转为 URL 友好格式', () => {
expect(slugifyPostTitle('Hello World')).toBe('hello-world')
expect(slugifyPostTitle('React 入门指南')).toBe('react')
})
何时做:开发阶段,每写一个函数就写对应测试。
价值:重构时快速确认没有破坏现有逻辑。
集成测试(Integration Test)
测试多个模块组合后的行为。关注模块间的接口和数据流转。
// 示例:测试 API 路由与数据库的集成
test('POST /api/posts 应创建文章并写入数据库', async () => {
const res = await POST(createRequest({ title: '测试', content: '...' }))
expect(res.status).toBe(200)
const post = await prisma.post.findUnique({ where: { slug: '测试' } })
expect(post).not.toBeNull()
})
何时做:开发后期,模块基本完成后。
价值:捕获模块间接口不匹配的问题。
端到端测试(E2E Test)
模拟真实用户从头到尾操作整个流程。
// 示例:用 Playwright 测试完整发文流程
test('管理员可以登录并发布文章', async ({ page }) => {
await page.goto('/admin')
await page.fill('input[name=password]', 'xxx')
await page.click('button[type=submit]')
await page.click('text=新建文章')
await page.fill('input[name=title]', '我的文章')
await page.click('text=发布')
await expect(page).toHaveURL(/\/blog\/我的文章/)
})
何时做:上线前。
价值:验证完整用户旅程是否畅通。这是单元测试和集成测试无法替代的。
回归测试(Regression Test)
修改代码后确认旧功能没被破坏。
何时做:每次修改代码后、每次合并 PR 前。
实践:通常通过 CI(持续集成)自动运行全部已有测试。如果测试通过,说明修改没有引入回归。
性能测试(Performance Test)
测试高并发下的响应时间和稳定性。
# 示例:用 wrk 或 k6 做压测
wrk -t12 -c50 -d30s https://liaoqizai.site/zh
# 结果示例:
# 50 并发下错误率 38.79%,p95 延迟 13.24s
# → 瓶颈在 SSR 渲染,需要加 CDN 缓存或 ISR
何时做:上线前。
关键指标:
- 吞吐量:每秒处理请求数(req/s)
- 延迟:p50/p95/p99 响应时间
- 错误率:失败请求占比
- 恢复时间:停止攻击后多久恢复正常
安全测试(Security Test)
发现可被攻击者利用的漏洞。
何时做:上线前,且定期复测。
包含:渗透测试(黑盒)、代码安全审计(白盒)、依赖扫描、配置审计。详见下文安全视角部分。
三、按安全视角划分
安全测试是一个独立且专业的维度,以下按方法分类:
SAST(静态应用安全测试)
即白盒安全审计。通过分析源码发现安全缺陷。
工具:
- SonarQube:代码质量和安全扫描
- ESLint security 插件:前端安全规则
- Semgrep:多语言安全扫描
能发现:硬编码密钥、SQL 注入风险、XSS 漏洞、不安全的加密用法。
局限:无法发现运行时行为导致的问题。
DAST(动态应用安全测试)
即黑盒安全扫描。从外部向运行中的应用发送恶意请求,观察响应。
工具:
- OWASP ZAP:开源 Web 应用扫描器
- Burp Suite:专业渗透测试工具
- nuclei:基于模板的漏洞扫描器
能发现:SQL 注入、XSS、认证绕过、目录遍历等运行时漏洞。
局限:无法发现代码内部隐患,覆盖率取决于扫描规则。
SCA(软件成分分析)
检查第三方依赖是否有已知漏洞。
# 示例:npm audit
npm audit
# 输出示例:
# 2 high severity vulnerabilities
# sharp → libvips 依赖链有已知漏洞
工具:
- npm audit:Node.js 生态
- Snyk:多语言依赖扫描
- Dependabot:GitHub 自动依赖更新
关键点:发现漏洞后不应盲目 npm audit fix(可能引入 breaking change),需评估实际可达性。
渗透测试(Penetration Test)
模拟真实攻击者的手法,尝试从外部攻破系统。
与 DAST 的区别:渗透测试由人工驱动,会根据目标特征调整攻击策略;DAST 是自动化扫描。
典型流程:
信息收集(OSINT)
→ 端口扫描 / 服务指纹识别
→ 漏洞探测
→ 漏洞利用
→ 后渗透(评估影响范围)
→ 清理痕迹 + 报告
配置审计
检查服务器、Nginx、Docker、云服务等基础设施配置是否安全。
检查清单:
- Nginx:HSTS、CSP、X-Frame-Options、server_tokens off
- Docker:镜像不含敏感文件、不暴露不必要的端口
- 云服务:镜像仓库访问权限、数据库公网暴露、IAM 策略
- TLS:证书有效性、密码套件、协议版本
OSINT(开源情报收集)
搜索公开渠道泄露的信息。
检查项:
- GitHub/Gitee 仓库是否公开(源码、CI/CD 配置、邮箱)
- Git 历史是否曾提交过密钥
- 搜索引擎是否索引了敏感页面
- 域名 DNS 记录是否泄露内部架构
四、按自动化程度划分
手动测试
人手工操作和判断。代码审查、渗透测试的大部分工作都是手动。
特点:灵活,能发现自动化工具找不到的问题。但效率低、不可重复。
自动化测试
脚本自动执行和判断。单元测试、集成测试、E2E 测试都属于此类。
特点:可重复、可纳入 CI。但只能检查预定义的场景。
持续集成(CI)
每次提交代码自动运行全部测试。
# 示例:GitHub Actions
name: CI
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm ci
- run: npm run lint
- run: npm test
- run: npm run build
价值:在代码合并前自动拦截回归。上线前测试不是一次性工作,CI 让测试持续运行。
五、实际项目中的测试流程
以一个 Next.js 个人博客项目为例:
开发阶段
├── 写单元测试(每个工具函数)
├── 写集成测试(API 路由 + 数据库)
└── 本地跑 npm test 确认通过
代码审查阶段(白盒)
├── 逐行审查源码,关注:
│ ├── 递归是否有深度限制
│ ├── Map/缓存是否清理过期记录
│ ├── 输入校验是否完整
│ ├── 数据库是否有唯一约束
│ └── React 组件是否有 hydration 风险
└── 输出代码审查问题清单
部署前阶段
├── npm audit 检查依赖漏洞
├── 跑 E2E 测试(用户登录→发文→发布的完整流程)
└── 性能压测(wrk/k6 模拟并发)
上线后阶段(黑盒)
├── 渗透测试(多轮,模拟攻击者)
│ ├── 认证绕过尝试
│ ├── 注入测试(SQL/XSS/SSRF)
│ ├── 配置审计(TLS/Nginx/Docker)
│ ├── OSINT(GitHub/Gitee 是否泄露源码)
│ └── CI/CD 审计(镜像仓库权限、Git 历史)
└── 输出渗透测试报告
持续运维
├── CI 每次提交自动跑测试
├── 定期 npm audit
├── 定期渗透测试复测
└── 监控告警(异常请求、性能下降)
六、常见误区
误区一:有单元测试就够了
单元测试只验证单个函数的正确性。但模块间接口不匹配、运行时时序问题、配置缺陷都无法通过单元测试发现。需要集成测试、E2E 测试、渗透测试配合。
误区二:白盒和黑盒二选一
白盒能发现代码内部隐患,黑盒能发现运行时漏洞。两者互补,不是替代关系。只做白盒会漏掉运行时漏洞;只做黑盒会漏掉代码内部隐患。
误区三:渗透测试一次就够了
系统每次更新都可能引入新漏洞。渗透测试应定期复测,且每次大版本更新后追加针对性测试。
误区四:npm audit 报告的漏洞都要修
npm audit 报告的漏洞不一定可达。需评估:该依赖是否在生产环境使用?漏洞路径是否可达?如果生产镜像已移除相关包且禁用了相关功能,实际风险可能很低。不应盲目按 npm audit fix 降级依赖(可能引入 breaking change)。
误区五:代码没报错就是安全的
代码运行正常不代表安全。很多漏洞在正常使用下不会触发,只有攻击者构造特定输入才会暴露。如认证绕过、Host 头注入、CSRF 等,都需要专门的测试用例才能发现。
七、测试金字塔
一个健康的测试体系应呈金字塔形:
/\
/ \ E2E 测试(少量,慢,覆盖完整流程)
/----\
/ \ 集成测试(中等,验证模块组合)
/--------\
/ \ 单元测试(大量,快,验证单个函数)
/____________\
- 底层:大量单元测试,跑得快,覆盖每个函数。
- 中层:适量集成测试,验证模块间协作。
- 顶层:少量 E2E 测试,跑得慢,覆盖关键用户旅程。
倒金字塔(E2E 多、单元少)是反模式:测试慢、维护成本高、失败时难定位根因。
八、上线检查清单
最后给一份实用的上线前检查清单:
代码质量
- 单元测试全部通过
- 集成测试全部通过
- 关键流程有 E2E 测试覆盖
- 代码审查问题清单中的"值得修改"项已处理
安全
- npm audit 已检查,高危漏洞已评估可达性
- 白盒安全审计已完成
- 渗透测试已完成,真漏洞已修复
- GitHub/Gitee 仓库已设为 private(如需)
- Git 历史无硬编码密钥
- 所有密钥/密码已使用环境变量,未硬编码
基础设施
- Nginx 配置了 HSTS、CSP、X-Frame-Options 等安全头
- TLS 证书有效,密码套件使用 ECDHE(前向保密)
- Docker 镜像仓库关闭了匿名拉取
- server_tokens off
- client_max_body_size 与应用层限制一致
CI/CD
- 每次提交自动跑测试
- 构建失败时不部署
- 部署脚本不含敏感信息
运维
- 有日志收集和异常告警
- 有性能监控
- 有定期备份
- 有回滚方案