Heroku CI #

CI 概述 #

Heroku CI 是 Heroku 内置的持续集成服务,可以自动运行测试,确保代码质量。

CI 流程 #

text
┌─────────────────────────────────────────────────────┐
│              Heroku CI 流程                         │
├─────────────────────────────────────────────────────┤
│                                                     │
│  代码推送                                           │
│     │                                               │
│     ▼                                               │
│  ┌─────────────────────────────────────────────┐   │
│  │           创建测试环境                        │   │
│  │  ├── 创建临时 Dyno                           │   │
│  │  ├── 设置环境变量                            │   │
│  │  └── 配置 Add-ons                            │   │
│  └─────────────────────────────────────────────┘   │
│     │                                               │
│     ▼                                               │
│  ┌─────────────────────────────────────────────┐   │
│  │           运行测试                            │   │
│  │  ├── 安装依赖                                │   │
│  │  ├── 执行测试脚本                            │   │
│  │  └── 收集测试结果                            │   │
│  └─────────────────────────────────────────────┘   │
│     │                                               │
│     ├──────────────┬──────────────┐                │
│     │              │              │                │
│     ▼              ▼              ▼                │
│   测试通过       测试失败       测试跳过            │
│     │              │              │                │
│     ▼              ▼              ▼                │
│   允许部署      阻止部署      继续流程              │
│                                                     │
└─────────────────────────────────────────────────────┘

启用 CI #

在 Pipeline 中启用 #

bash
# 创建 Pipeline
heroku pipelines:create myapp-pipeline -a myapp-staging -s staging

# 启用 CI
heroku ci:enable -a myapp-staging

# 查看 CI 状态
heroku ci:info

app.json 配置 #

json
{
  "name": "myapp",
  "environments": {
    "test": {
      "scripts": {
        "test": "npm test"
      },
      "addons": [
        "heroku-postgresql:mini",
        "heroku-redis:mini"
      ],
      "env": {
        "NODE_ENV": "test"
      }
    }
  }
}

测试配置 #

Node.js 测试 #

json
// package.json
{
  "scripts": {
    "test": "jest",
    "test:coverage": "jest --coverage",
    "test:watch": "jest --watch"
  }
}
json
// app.json
{
  "environments": {
    "test": {
      "scripts": {
        "test": "npm run test:coverage"
      }
    }
  }
}

Python 测试 #

json
// app.json
{
  "environments": {
    "test": {
      "scripts": {
        "test": "pytest --cov=app tests/"
      }
    }
  }
}

Ruby 测试 #

json
// app.json
{
  "environments": {
    "test": {
      "scripts": {
        "test": "bundle exec rspec"
      }
    }
  }
}

测试数据库 #

配置测试数据库 #

json
// app.json
{
  "environments": {
    "test": {
      "addons": [
        "heroku-postgresql:mini"
      ],
      "env": {
        "NODE_ENV": "test"
      }
    }
  }
}

使用测试数据库 #

javascript
// jest 配置
module.exports = {
  testEnvironment: 'node',
  setupFilesAfterEnv: ['./tests/setup.js']
};

// tests/setup.js
const { Pool } = require('pg');

const pool = new Pool({
  connectionString: process.env.DATABASE_URL,
  ssl: { rejectUnauthorized: false }
});

beforeAll(async () => {
  // 创建测试表
  await pool.query(`
    CREATE TABLE IF NOT EXISTS users (
      id SERIAL PRIMARY KEY,
      email VARCHAR(255) UNIQUE NOT NULL,
      name VARCHAR(255)
    )
  `);
});

afterAll(async () => {
  await pool.end();
});

测试命令 #

查看测试运行 #

bash
# 查看最近的测试运行
heroku ci:runs

# 输出示例
# === CI Runs
# Run #    Status    Branch    Commit    Created
# ─────    ──────    ──────    ──────    ───────
# 42       passed    main      abc123    10 minutes ago
# 41       failed    main      def456    1 hour ago

# 查看特定测试运行详情
heroku ci:run 42

# 查看测试日志
heroku ci:run 42 --log

手动触发测试 #

bash
# 手动触发测试
heroku ci:run

# 指定分支
heroku ci:run --branch feature-branch

测试环境变量 #

配置测试环境变量 #

json
// app.json
{
  "environments": {
    "test": {
      "env": {
        "NODE_ENV": "test",
        "DATABASE_URL": {
          "from_database": {
            "name": "heroku-postgresql",
            "addon": "heroku-postgresql:mini"
          }
        }
      }
    }
  }
}

使用密钥 #

bash
# 设置测试环境密钥
heroku config:set TEST_API_KEY=xxx --app myapp-staging

# CI 会自动继承应用的环境变量

测试报告 #

JUnit 报告 #

javascript
// jest 配置
module.exports = {
  reporters: [
    'default',
    ['jest-junit', { outputDirectory: './reports' }]
  ]
};

覆盖率报告 #

javascript
// jest 配置
module.exports = {
  coverageReporters: ['text', 'lcov', 'html'],
  coverageDirectory: './coverage'
};

GitHub Actions 替代方案 #

完整 CI/CD 配置 #

yaml
# .github/workflows/ci.yml
name: CI

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  test:
    runs-on: ubuntu-latest
    
    services:
      postgres:
        image: postgres:16
        env:
          POSTGRES_PASSWORD: postgres
        options: >-
          --health-cmd pg_isready
          --health-interval 10s
          --health-timeout 5s
          --health-retries 5
        ports:
          - 5432:5432
      
      redis:
        image: redis:7
        options: >-
          --health-cmd "redis-cli ping"
          --health-interval 10s
          --health-timeout 5s
          --health-retries 5
        ports:
          - 6379:6379
    
    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: Run tests
        run: npm test
        env:
          DATABASE_URL: postgres://postgres:postgres@localhost:5432/test
          REDIS_URL: redis://localhost:6379
          NODE_ENV: test
      
      - name: Upload coverage
        uses: codecov/codecov-action@v3
        with:
          files: ./coverage/lcov.info

  deploy:
    needs: test
    runs-on: ubuntu-latest
    if: github.ref == 'refs/heads/main'
    
    steps:
      - uses: actions/checkout@v4
      
      - name: Deploy to Heroku
        uses: akhileshns/heroku-deploy@v3.13.15
        with:
          heroku_api_key: ${{ secrets.HEROKU_API_KEY }}
          heroku_app_name: myapp-staging
          heroku_email: ${{ secrets.HEROKU_EMAIL }}

测试最佳实践 #

1. 测试结构 #

text
tests/
├── unit/           # 单元测试
│   ├── user.test.js
│   └── product.test.js
├── integration/    # 集成测试
│   ├── api.test.js
│   └── auth.test.js
└── e2e/           # 端到端测试
    └── flow.test.js

2. 测试脚本 #

json
// package.json
{
  "scripts": {
    "test": "jest",
    "test:unit": "jest tests/unit",
    "test:integration": "jest tests/integration",
    "test:e2e": "jest tests/e2e",
    "test:coverage": "jest --coverage",
    "test:watch": "jest --watch"
  }
}

3. 测试数据库隔离 #

javascript
// 每个测试使用独立的数据库
beforeEach(async () => {
  await pool.query('BEGIN');
});

afterEach(async () => {
  await pool.query('ROLLBACK');
});

故障排查 #

测试超时 #

javascript
// 增加测试超时时间
jest.setTimeout(30000);

// 或在特定测试中
test('long running test', async () => {
  // ...
}, 30000);

数据库连接问题 #

javascript
// 检查数据库连接
beforeAll(async () => {
  try {
    await pool.query('SELECT 1');
    console.log('Database connected');
  } catch (error) {
    console.error('Database connection failed:', error);
    throw error;
  }
});

环境变量问题 #

bash
# 检查 CI 环境变量
heroku ci:run 42 --log | grep -i env

下一步 #

CI 掌握后,接下来学习 私有空间 了解网络隔离配置!

最后更新:2026-03-28