18 KiB
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)。
# 获取容器 IP
docker inspect <container_name> --format "{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}"
# 示例输出: 172.17.0.2
2. 数据库连接配置
# 错误配置 (在 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 隔离
# 测试 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)
# 获取 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 容器
# 在 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. 验证容器运行状态
# 检查容器状态
docker ps
# 检查 PostgreSQL 是否就绪
docker exec wallet-postgres-test pg_isready -U wallet
3. 获取容器 IP
docker inspect wallet-postgres-test --format "{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}"
4. 推送 Prisma Schema
# 在 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:
# .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。
运行测试
# 在 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`
解决方案:
- 确认 Docker 容器正在运行:
docker ps - 获取容器 IP:
docker inspect <container> --format "{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}" - 更新 DATABASE_URL 使用容器 IP
问题 2: 容器 IP 地址变化
每次重启 Docker 容器后,IP 地址可能会改变。
解决方案:
- 使用 Docker network 创建固定网络
- 或在测试脚本中动态获取 IP
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 超时时间
// 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。
// ❌ 错误做法 - 重复添加
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 原生文件系统 (~/):
# 复制到 WSL2 原生文件系统后
npm install # ~40秒 (vs 超时)
npx jest # ~7秒 (vs 超时)
性能对比:
| 操作 | /mnt/c/ (Windows) | ~/ (WSL2 原生) |
|---|---|---|
| npm install | 超时 | 40秒 |
| Jest E2E 测试 | 超时 | 6.7秒 |
| 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:
#!/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
最佳实践
- 使用独立的测试数据库: 不要在开发或生产数据库上运行 E2E 测试
- 每次测试前后清理数据: 确保测试隔离性
- 使用唯一的测试 ID: 避免与其他数据冲突
- 正确处理异步操作: 确保所有 Promise 都被等待
- 关闭数据库连接: 在
afterAll中关闭应用和数据库连接 - 使用
--forceExit: 防止 Jest 挂起
参考命令
# 查看所有 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 超时
解决方案:
- 首选: 将项目放在 WSL2 的原生文件系统中 (
~/projects/而不是/mnt/c/) - 或者: 从 Windows 直接运行测试,但需要配置正确的数据库连接
# 复制项目到 WSL2 原生文件系统
cp -r /mnt/c/Users/<user>/project ~/project-wsl
cd ~/project-wsl
npm install
npm run test:e2e
单元测试 vs E2E 测试
由于上述性能问题,建议:
- 单元测试: 使用 Mock,不需要真实数据库,可以在 Windows 上快速运行
- 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 原生文件系统可以完美解决性能和网络问题!
# 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
推荐方案
- 本地开发: 将项目复制到 WSL2 原生文件系统运行真实数据库 E2E 测试
- CI/CD: 在 GitHub Actions 中直接使用 localhost 连接 PostgreSQL 服务
- 日常开发: 单元测试可在 Windows 上直接运行,无需数据库
CI/CD 集成建议
在 GitHub Actions 中运行真实数据库 E2E 测试:
# .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)
调试技巧
验证数据库连接
// 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 应用启动
// 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);