557 lines
18 KiB
Markdown
557 lines
18 KiB
Markdown
# E2E Testing with WSL2 and Docker PostgreSQL
|
||
|
||
本文档记录了在 Windows + WSL2 环境下使用真实 PostgreSQL 数据库进行 E2E 测试的经验和最佳实践。
|
||
|
||
## 环境架构
|
||
|
||
```
|
||
┌─────────────────────────────────────────────────────────┐
|
||
│ Windows Host │
|
||
│ ┌─────────────────────────────────────────────────────┐│
|
||
│ │ WSL2 ││
|
||
│ │ ┌─────────────────────────────────────────────────┐││
|
||
│ │ │ Docker Engine │││
|
||
│ │ │ ┌───────────────────────────────────────────┐ │││
|
||
│ │ │ │ PostgreSQL Container (172.17.0.x) │ │││
|
||
│ │ │ │ Port: 5432 │ │││
|
||
│ │ │ └───────────────────────────────────────────┘ │││
|
||
│ │ └─────────────────────────────────────────────────┘││
|
||
│ │ ││
|
||
│ │ Node.js Application (测试运行环境) ││
|
||
│ │ 通过 Docker 网络 (172.17.0.x) 连接 PostgreSQL ││
|
||
│ └─────────────────────────────────────────────────────┘│
|
||
└─────────────────────────────────────────────────────────┘
|
||
```
|
||
|
||
## 重要发现
|
||
|
||
### 1. 网络连接问题
|
||
|
||
**问题**: 在 WSL2 内运行的 Node.js 应用程序无法通过 `localhost:5432` 连接到 Docker 容器内的 PostgreSQL。
|
||
|
||
**原因**: WSL2 的 localhost 和 Docker 容器的网络是隔离的。即使 Docker 端口映射到 `0.0.0.0:5432`,WSL2 内的应用仍然无法通过 localhost 访问。
|
||
|
||
**解决方案**: 使用 Docker 容器的实际 IP 地址(通常是 172.17.0.x)。
|
||
|
||
```bash
|
||
# 获取容器 IP
|
||
docker inspect <container_name> --format "{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}"
|
||
|
||
# 示例输出: 172.17.0.2
|
||
```
|
||
|
||
### 2. 数据库连接配置
|
||
|
||
```bash
|
||
# 错误配置 (在 WSL2 中无法工作)
|
||
DATABASE_URL="postgresql://user:pass@localhost:5432/dbname"
|
||
|
||
# 正确配置 (使用 Docker 容器 IP)
|
||
DATABASE_URL="postgresql://user:pass@172.17.0.2:5432/dbname"
|
||
```
|
||
|
||
### 3. Windows 和 WSL2 网络隔离 (重要发现!)
|
||
|
||
**关键发现**: Windows 和 WSL2 之间存在网络隔离,这是在 E2E 测试中遇到的核心挑战。
|
||
|
||
#### 三种运行环境的网络访问方式
|
||
|
||
| 测试运行位置 | 数据库地址 | 是否可用 |
|
||
|-------------|-----------|---------|
|
||
| WSL2 内部 | `172.17.0.x` (Docker 容器 IP) | ✅ 可用 |
|
||
| WSL2 内部 | `localhost:5432` | ❌ 不可用 |
|
||
| Windows 原生 | `localhost:5432` | ❌ 不可用 (无 Docker Desktop) |
|
||
| Windows 原生 | WSL2 IP (172.24.x.x) | ❌ 不可用 (网络隔离) |
|
||
| CI/CD (Linux) | `localhost:5432` | ✅ 可用 |
|
||
|
||
#### 详细说明
|
||
|
||
**从 Windows 访问 WSL2 中的 Docker 容器**:
|
||
- ❌ Windows **无法**通过 `localhost:5432` 访问 WSL2 中的 Docker 容器
|
||
- ❌ Windows **无法**通过 WSL2 IP (如 172.24.157.5:5432) 访问
|
||
- 原因: WSL2 使用 NAT 网络模式,网络与 Windows 隔离
|
||
|
||
```powershell
|
||
# 测试 Windows 到 WSL2 的网络连接
|
||
Test-NetConnection -ComputerName 172.24.157.5 -Port 5432
|
||
# 输出: TCP connect to (172.24.157.5 : 5432) failed
|
||
# 输出: Ping to 172.24.157.5 failed with status: DestinationNetworkUnreachable
|
||
```
|
||
|
||
**从 WSL2 访问 Docker 容器**:
|
||
- ❌ 不能使用 localhost
|
||
- ✅ 必须使用容器的实际 IP 地址 (172.17.0.x)
|
||
|
||
```bash
|
||
# 获取 WSL2 的 IP 地址
|
||
hostname -I
|
||
# 输出: 172.24.157.5 172.19.0.1 172.17.0.1 172.18.0.1
|
||
|
||
# 获取 Docker 容器的 IP 地址
|
||
docker inspect wallet-postgres-test --format "{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}"
|
||
# 输出: 172.17.0.2
|
||
```
|
||
|
||
#### 解决方案对比
|
||
|
||
| 方案 | 优点 | 缺点 | 推荐 |
|
||
|-----|------|------|-----|
|
||
| 在 WSL2 原生文件系统运行测试 | 性能最好,网络直连 | 需要复制代码 | ⭐⭐⭐ |
|
||
| 使用 Docker Desktop (Windows) | localhost 可用 | 需要额外安装 | ⭐⭐ |
|
||
| CI/CD 环境 (GitHub Actions) | 环境一致,网络简单 | 本地无法测试 | ⭐⭐⭐ |
|
||
| Mock 测试 | 无需真实数据库 | 测试不全面 | ⭐ |
|
||
|
||
## Docker 设置步骤
|
||
|
||
### 1. 创建 PostgreSQL 容器
|
||
|
||
```bash
|
||
# 在 WSL2 中运行
|
||
docker run -d \
|
||
--restart=always \
|
||
--name wallet-postgres-test \
|
||
-e POSTGRES_USER=wallet \
|
||
-e POSTGRES_PASSWORD=wallet123 \
|
||
-e POSTGRES_DB=wallet_test \
|
||
-p 5432:5432 \
|
||
postgres:15-alpine
|
||
```
|
||
|
||
### 2. 验证容器运行状态
|
||
|
||
```bash
|
||
# 检查容器状态
|
||
docker ps
|
||
|
||
# 检查 PostgreSQL 是否就绪
|
||
docker exec wallet-postgres-test pg_isready -U wallet
|
||
```
|
||
|
||
### 3. 获取容器 IP
|
||
|
||
```bash
|
||
docker inspect wallet-postgres-test --format "{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}"
|
||
```
|
||
|
||
### 4. 推送 Prisma Schema
|
||
|
||
```bash
|
||
# 在 WSL2 中运行
|
||
cd /mnt/c/Users/<username>/path/to/project
|
||
export DATABASE_URL='postgresql://wallet:wallet123@172.17.0.2:5432/wallet_test?schema=public'
|
||
npx prisma db push --force-reset
|
||
```
|
||
|
||
## E2E 测试运行
|
||
|
||
### 环境变量配置
|
||
|
||
根据不同的运行环境,需要配置不同的 `DATABASE_URL`:
|
||
|
||
```bash
|
||
# .env.test 配置 (根据运行环境选择)
|
||
|
||
# 1. 在 WSL2 中运行测试 - 使用 Docker 容器 IP
|
||
DATABASE_URL="postgresql://wallet:wallet123@172.17.0.2:5432/wallet_test?schema=public"
|
||
|
||
# 2. 在 CI/CD (GitHub Actions, Linux) 中运行 - 使用 localhost
|
||
DATABASE_URL="postgresql://wallet:wallet123@localhost:5432/wallet_test?schema=public"
|
||
|
||
# 3. 通用配置
|
||
JWT_SECRET="test-jwt-secret-key-for-e2e-testing"
|
||
NODE_ENV=test
|
||
PORT=3001
|
||
```
|
||
|
||
**注意**: NestJS ConfigModule 会根据 `NODE_ENV` 加载对应的 `.env.{NODE_ENV}` 文件,所以设置 `NODE_ENV=test` 会自动加载 `.env.test`。
|
||
|
||
### 运行测试
|
||
|
||
```bash
|
||
# 在 WSL2 中运行 (确保 .env.test 使用容器 IP)
|
||
npm run test:e2e
|
||
# 或
|
||
npx jest --config ./test/jest-e2e.json --runInBand --forceExit
|
||
```
|
||
|
||
## 常见问题排查
|
||
|
||
### 问题 1: 无法连接数据库
|
||
|
||
```
|
||
Error: P1001: Can't reach database server at `localhost:5432`
|
||
```
|
||
|
||
**解决方案**:
|
||
1. 确认 Docker 容器正在运行: `docker ps`
|
||
2. 获取容器 IP: `docker inspect <container> --format "{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}"`
|
||
3. 更新 DATABASE_URL 使用容器 IP
|
||
|
||
### 问题 2: 容器 IP 地址变化
|
||
|
||
每次重启 Docker 容器后,IP 地址可能会改变。
|
||
|
||
**解决方案**:
|
||
- 使用 Docker network 创建固定网络
|
||
- 或在测试脚本中动态获取 IP
|
||
|
||
```bash
|
||
CONTAINER_IP=$(docker inspect wallet-postgres-test --format "{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}")
|
||
export DATABASE_URL="postgresql://wallet:wallet123@$CONTAINER_IP:5432/wallet_test?schema=public"
|
||
```
|
||
|
||
### 问题 3: Jest 测试挂起
|
||
|
||
**可能原因**:
|
||
- 数据库连接超时
|
||
- 未正确关闭数据库连接
|
||
- 异步操作未完成
|
||
|
||
**解决方案**:
|
||
- 添加 `--forceExit` 参数
|
||
- 在 `afterAll` 中确保调用 `app.close()`
|
||
- 增加 Jest 超时时间
|
||
|
||
```json
|
||
// jest-e2e.json
|
||
{
|
||
"testTimeout": 30000,
|
||
"verbose": true
|
||
}
|
||
```
|
||
|
||
### 问题 4: 测试断言失败 - 响应结构不匹配
|
||
|
||
**错误现象**:
|
||
```
|
||
expect(res.body.data).toHaveProperty('walletId');
|
||
// 失败: Received path: []
|
||
// 但实际 res.body.data 包含 { walletId: "1", ... }
|
||
```
|
||
|
||
**原因**: 在 E2E 测试中手动添加了 `TransformInterceptor`,但 `AppModule` 已通过 `APP_INTERCEPTOR` 全局提供,导致响应被双重包装。
|
||
|
||
**解决方案**: 不要在测试中重复添加已由 AppModule 全局提供的 Filter 和 Interceptor。
|
||
|
||
```typescript
|
||
// ❌ 错误做法 - 重复添加
|
||
app.useGlobalFilters(new DomainExceptionFilter());
|
||
app.useGlobalInterceptors(new TransformInterceptor());
|
||
|
||
// ✅ 正确做法 - 只添加 ValidationPipe
|
||
app.useGlobalPipes(
|
||
new ValidationPipe({
|
||
whitelist: true,
|
||
forbidNonWhitelisted: true,
|
||
transform: true,
|
||
}),
|
||
);
|
||
// DomainExceptionFilter 和 TransformInterceptor 已由 AppModule 提供
|
||
```
|
||
|
||
### 问题 5: WSL2 跨文件系统性能极差
|
||
|
||
**错误现象**:
|
||
```
|
||
# 在 WSL2 中访问 /mnt/c/ 运行测试
|
||
npm install # 超时
|
||
npx jest # 超时 (120秒+)
|
||
```
|
||
|
||
**原因**: WSL2 的 `/mnt/c/` 是通过 9P 协议挂载的 Windows 文件系统,I/O 性能比原生 Linux 文件系统慢 10-100 倍。
|
||
|
||
**解决方案**: 将项目复制到 WSL2 原生文件系统 (`~/`):
|
||
|
||
```bash
|
||
# 复制到 WSL2 原生文件系统后
|
||
npm install # ~40秒 (vs 超时)
|
||
npx jest # ~7秒 (vs 超时)
|
||
```
|
||
|
||
**性能对比**:
|
||
| 操作 | /mnt/c/ (Windows) | ~/ (WSL2 原生) |
|
||
|-----|-------------------|----------------|
|
||
| npm install | 超时 | 40秒 |
|
||
| Jest E2E 测试 | 超时 | 6.7秒 |
|
||
| TypeScript 编译 | 极慢 | 正常 |
|
||
|
||
## 测试数据清理
|
||
|
||
```typescript
|
||
async function cleanupTestData() {
|
||
try {
|
||
await prisma.ledgerEntry.deleteMany({ where: { userId: BigInt(testUserId) } });
|
||
await prisma.depositOrder.deleteMany({ where: { userId: BigInt(testUserId) } });
|
||
await prisma.settlementOrder.deleteMany({ where: { userId: BigInt(testUserId) } });
|
||
await prisma.walletAccount.deleteMany({ where: { userId: BigInt(testUserId) } });
|
||
} catch (e) {
|
||
console.log('Cleanup error (may be expected):', e);
|
||
}
|
||
}
|
||
```
|
||
|
||
## 自动化脚本
|
||
|
||
创建 `scripts/test-e2e.sh`:
|
||
|
||
```bash
|
||
#!/bin/bash
|
||
|
||
# 获取容器 IP
|
||
CONTAINER_IP=$(docker inspect wallet-postgres-test --format "{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}" 2>/dev/null)
|
||
|
||
if [ -z "$CONTAINER_IP" ]; then
|
||
echo "Error: PostgreSQL container not running. Starting..."
|
||
docker start wallet-postgres-test || docker run -d \
|
||
--restart=always \
|
||
--name wallet-postgres-test \
|
||
-e POSTGRES_USER=wallet \
|
||
-e POSTGRES_PASSWORD=wallet123 \
|
||
-e POSTGRES_DB=wallet_test \
|
||
-p 5432:5432 \
|
||
postgres:15-alpine
|
||
|
||
sleep 5
|
||
CONTAINER_IP=$(docker inspect wallet-postgres-test --format "{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}")
|
||
fi
|
||
|
||
echo "Using PostgreSQL at: $CONTAINER_IP"
|
||
|
||
export DATABASE_URL="postgresql://wallet:wallet123@$CONTAINER_IP:5432/wallet_test?schema=public"
|
||
export JWT_SECRET="test-jwt-secret-key-for-e2e-testing"
|
||
export NODE_ENV=test
|
||
|
||
# 推送 schema (如果需要)
|
||
npx prisma db push --skip-generate
|
||
|
||
# 运行测试
|
||
npx jest --config ./test/jest-e2e.json --runInBand --forceExit
|
||
```
|
||
|
||
## 最佳实践
|
||
|
||
1. **使用独立的测试数据库**: 不要在开发或生产数据库上运行 E2E 测试
|
||
2. **每次测试前后清理数据**: 确保测试隔离性
|
||
3. **使用唯一的测试 ID**: 避免与其他数据冲突
|
||
4. **正确处理异步操作**: 确保所有 Promise 都被等待
|
||
5. **关闭数据库连接**: 在 `afterAll` 中关闭应用和数据库连接
|
||
6. **使用 `--forceExit`**: 防止 Jest 挂起
|
||
|
||
## 参考命令
|
||
|
||
```bash
|
||
# 查看所有 Docker 容器
|
||
docker ps -a
|
||
|
||
# 查看容器日志
|
||
docker logs wallet-postgres-test
|
||
|
||
# 进入 PostgreSQL 容器
|
||
docker exec -it wallet-postgres-test psql -U wallet -d wallet_test
|
||
|
||
# 重置数据库
|
||
docker exec wallet-postgres-test psql -U wallet -c "DROP DATABASE IF EXISTS wallet_test; CREATE DATABASE wallet_test;"
|
||
```
|
||
|
||
## 性能问题
|
||
|
||
### WSL2 跨文件系统访问慢
|
||
|
||
**问题**: 在 WSL2 中访问 `/mnt/c/` (Windows 文件系统) 比访问 Linux 原生文件系统慢很多。
|
||
|
||
**现象**:
|
||
- `npm run build` 和 `npx jest` 启动很慢
|
||
- 编译 TypeScript 需要很长时间
|
||
- 测试可能因为 I/O 超时
|
||
|
||
**解决方案**:
|
||
1. **首选**: 将项目放在 WSL2 的原生文件系统中 (`~/projects/` 而不是 `/mnt/c/`)
|
||
2. **或者**: 从 Windows 直接运行测试,但需要配置正确的数据库连接
|
||
|
||
```bash
|
||
# 复制项目到 WSL2 原生文件系统
|
||
cp -r /mnt/c/Users/<user>/project ~/project-wsl
|
||
cd ~/project-wsl
|
||
npm install
|
||
npm run test:e2e
|
||
```
|
||
|
||
### 单元测试 vs E2E 测试
|
||
|
||
由于上述性能问题,建议:
|
||
|
||
1. **单元测试**: 使用 Mock,不需要真实数据库,可以在 Windows 上快速运行
|
||
2. **E2E 测试**: 使用真实数据库,适合 CI/CD 环境或原生 Linux 环境
|
||
|
||
## 当前状态总结
|
||
|
||
### ✅ 全部测试通过!
|
||
|
||
| 测试类型 | 数量 | 状态 | 运行时间 |
|
||
|---------|------|------|---------|
|
||
| 单元测试 | 69 | ✅ 通过 | 5.2s |
|
||
| E2E 测试 (真实数据库) | 23 | ✅ 通过 | 6.7s |
|
||
|
||
### 已完成
|
||
- ✅ PostgreSQL Docker 容器创建和运行
|
||
- ✅ 数据库 Schema 推送成功
|
||
- ✅ 单独脚本可以成功连接数据库 (从 WSL2 内部)
|
||
- ✅ 单元测试 (69 个) 全部通过
|
||
- ✅ **E2E 真实数据库测试 (23 个) 全部通过!** 🎉
|
||
|
||
### 网络问题分析
|
||
- ❌ Windows → WSL2 Docker: 网络不可达 (NAT 隔离)
|
||
- ❌ WSL2 → Docker via localhost: 不可用
|
||
- ✅ WSL2 → Docker via 容器 IP (172.17.0.x): 可用
|
||
- ✅ 从 WSL2 原生文件系统运行测试: 性能极佳
|
||
|
||
### 解决方案总结
|
||
|
||
**关键发现**: 将项目复制到 WSL2 原生文件系统可以完美解决性能和网络问题!
|
||
|
||
```bash
|
||
# 1. 复制项目到 WSL2 原生文件系统
|
||
mkdir -p ~/wallet-service-test
|
||
cp -r /mnt/c/Users/<user>/project/src ~/wallet-service-test/
|
||
cp -r /mnt/c/Users/<user>/project/test ~/wallet-service-test/
|
||
cp -r /mnt/c/Users/<user>/project/prisma ~/wallet-service-test/
|
||
cp /mnt/c/Users/<user>/project/package*.json ~/wallet-service-test/
|
||
cp /mnt/c/Users/<user>/project/tsconfig*.json ~/wallet-service-test/
|
||
cp /mnt/c/Users/<user>/project/nest-cli.json ~/wallet-service-test/
|
||
cp /mnt/c/Users/<user>/project/.env.test ~/wallet-service-test/
|
||
|
||
# 2. 安装依赖 (~40秒)
|
||
cd ~/wallet-service-test
|
||
npm install
|
||
|
||
# 3. 生成 Prisma Client 并推送 Schema
|
||
export DATABASE_URL='postgresql://wallet:wallet123@172.17.0.2:5432/wallet_test?schema=public'
|
||
npx prisma generate
|
||
npx prisma db push --skip-generate
|
||
|
||
# 4. 运行 E2E 测试 (~7秒)
|
||
export JWT_SECRET='test-jwt-secret-key-for-e2e-testing'
|
||
export NODE_ENV=test
|
||
npx jest --config ./test/jest-e2e.json --runInBand --forceExit
|
||
```
|
||
|
||
### 推荐方案
|
||
1. **本地开发**: 将项目复制到 WSL2 原生文件系统运行真实数据库 E2E 测试
|
||
2. **CI/CD**: 在 GitHub Actions 中直接使用 localhost 连接 PostgreSQL 服务
|
||
3. **日常开发**: 单元测试可在 Windows 上直接运行,无需数据库
|
||
|
||
## CI/CD 集成建议
|
||
|
||
在 GitHub Actions 中运行真实数据库 E2E 测试:
|
||
|
||
```yaml
|
||
# .github/workflows/e2e-tests.yml
|
||
name: E2E Tests
|
||
|
||
on:
|
||
push:
|
||
branches: [main, develop]
|
||
pull_request:
|
||
branches: [main]
|
||
|
||
jobs:
|
||
e2e:
|
||
runs-on: ubuntu-latest
|
||
|
||
services:
|
||
postgres:
|
||
image: postgres:15-alpine
|
||
env:
|
||
POSTGRES_USER: wallet
|
||
POSTGRES_PASSWORD: wallet123
|
||
POSTGRES_DB: wallet_test
|
||
ports:
|
||
- 5432:5432
|
||
options: >-
|
||
--health-cmd pg_isready
|
||
--health-interval 10s
|
||
--health-timeout 5s
|
||
--health-retries 5
|
||
|
||
steps:
|
||
- uses: actions/checkout@v4
|
||
|
||
- name: Setup Node.js
|
||
uses: actions/setup-node@v4
|
||
with:
|
||
node-version: '20'
|
||
cache: 'npm'
|
||
|
||
- name: Install dependencies
|
||
run: npm ci
|
||
|
||
- name: Push Prisma schema
|
||
run: npx prisma db push
|
||
env:
|
||
DATABASE_URL: postgresql://wallet:wallet123@localhost:5432/wallet_test?schema=public
|
||
|
||
- name: Run E2E tests
|
||
run: npm run test:e2e
|
||
env:
|
||
DATABASE_URL: postgresql://wallet:wallet123@localhost:5432/wallet_test?schema=public
|
||
JWT_SECRET: test-jwt-secret
|
||
NODE_ENV: test
|
||
```
|
||
|
||
## 测试文件结构
|
||
|
||
```
|
||
test/
|
||
├── jest-e2e.json # E2E 测试配置
|
||
├── app.e2e-spec.ts # 真实数据库 E2E 测试
|
||
└── (其他测试文件)
|
||
|
||
src/
|
||
├── domain/
|
||
│ ├── aggregates/*.spec.ts # 领域聚合单元测试
|
||
│ └── value-objects/*.spec.ts # 值对象单元测试
|
||
└── application/
|
||
└── services/*.spec.ts # 应用服务集成测试 (Mock)
|
||
```
|
||
|
||
## 调试技巧
|
||
|
||
### 验证数据库连接
|
||
```javascript
|
||
// test-db.js
|
||
const { PrismaClient } = require('@prisma/client');
|
||
const prisma = new PrismaClient();
|
||
|
||
async function main() {
|
||
console.log('DATABASE_URL:', process.env.DATABASE_URL);
|
||
console.log('Connecting...');
|
||
await prisma.$connect();
|
||
console.log('Connected!');
|
||
const result = await prisma.$queryRaw`SELECT 1 as test`;
|
||
console.log('Query result:', result);
|
||
await prisma.$disconnect();
|
||
}
|
||
|
||
main().catch(console.error);
|
||
```
|
||
|
||
### 验证 NestJS 应用启动
|
||
```javascript
|
||
// test-nest-startup.js
|
||
const { Test } = require('@nestjs/testing');
|
||
const { AppModule } = require('./dist/app.module');
|
||
|
||
async function main() {
|
||
const moduleFixture = await Test.createTestingModule({
|
||
imports: [AppModule],
|
||
}).compile();
|
||
|
||
const app = moduleFixture.createNestApplication();
|
||
await app.init();
|
||
console.log('App initialized successfully!');
|
||
await app.close();
|
||
}
|
||
|
||
main().catch(console.error);
|
||
```
|