rwadurian/backend/services/wallet-service/docs/E2E-TESTING-WSL2.md

557 lines
18 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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);
```