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