整合測試(Integration Testing)

是什麼?

整合測試(Integration Testing)驗證多個元件、模組或服務之間的互動是否正確。它比單元測試涵蓋更多的真實依賴,但比 E2E 測試範圍更小、速度更快。

ℹ️測試金字塔中的中層

整合測試位於測試金字塔的中間層,是投資報酬率最高的測試類型之一。它能捕捉到單元測試漏掉的互動問題,又比 E2E 快得多。

核心觀念

常見誤區

⚠️整合測試的陷阱

  • 整合測試不是「大型單元測試」:不要把所有依賴都 Mock 掉,那就只是偽裝成整合測試的單元測試
  • 測試資料互相汙染:每個測試必須有獨立的資料,用 transaction rollback 或重建資料庫確保隔離
  • 環境依賴:不要依賴外部服務的實際狀態,用 Testcontainers 或 Docker Compose 自帶環境
  • 太慢就不跑:整合測試如果跑超過 5 分鐘,團隊就不會常跑,失去價值

實作流程

1

選擇測試範圍

決定要測哪些元件之間的互動(如 API → Service → DB)

2

準備測試環境

用 Testcontainers 或 In-Memory DB 啟動依賴

3

準備測試資料

Seed 必要的初始資料,確保每個測試獨立

4

執行整合操作

透過 API 或 Service 層發起真實的操作

5

驗證跨元件結果

檢查資料庫狀態、回應內容、事件是否正確發送

6

清理環境

還原資料庫或銷毀容器,為下一個測試做準備

程式碼範例

C#(xUnit + WebApplicationFactory)

csharp
// 使用 WebApplicationFactory 啟動真實的 HTTP pipeline
public class OrderApiIntegrationTests
    : IClassFixture<WebApplicationFactory<Program>>
{
    private readonly HttpClient _client;
    private readonly WebApplicationFactory<Program> _factory;
 
    public OrderApiIntegrationTests(WebApplicationFactory<Program> factory)
    {
        _factory = factory.WithWebHostBuilder(builder =>
        {
            builder.ConfigureServices(services =>
            {
                // 替換真實 DB 為 In-Memory DB
                services.RemoveAll<DbContextOptions<AppDbContext>>();
                services.AddDbContext<AppDbContext>(options =>
                    options.UseInMemoryDatabase("TestDb"));
            });
        });
        _client = _factory.CreateClient();
    }
 
    [Fact]
    public async Task CreateOrder_ValidProduct_ReturnsCreatedWithOrderId()
    {
        // Arrange: Seed 商品資料
        using var scope = _factory.Services.CreateScope();
        var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
        db.Products.Add(new Product { Id = 1, Name = "Book", Price = 299 });
        await db.SaveChangesAsync();
 
        // Act: 透過 API 建立訂單
        var response = await _client.PostAsJsonAsync("/api/orders", new
        {
            ProductId = 1,
            Quantity = 2
        });
 
        // Assert: 驗證 HTTP 回應和資料庫狀態
        Assert.Equal(HttpStatusCode.Created, response.StatusCode);
        var order = await response.Content.ReadFromJsonAsync<OrderDto>();
        Assert.Equal(598, order!.Total);
    }
}

TypeScript(Jest + Supertest)

typescript
import request from "supertest";
import { app } from "../src/app";
import { prisma } from "../src/db";
 
describe("POST /api/orders", () => {
  // 每個測試前清空資料
  beforeEach(async () => {
    await prisma.order.deleteMany();
    await prisma.product.deleteMany();
  });
 
  it("creates order for valid product", async () => {
    // Arrange: Seed 商品
    await prisma.product.create({
      data: { id: 1, name: "Book", price: 299 },
    });
 
    // Act
    const response = await request(app)
      .post("/api/orders")
      .send({ productId: 1, quantity: 2 });
 
    // Assert
    expect(response.status).toBe(201);
    expect(response.body.total).toBe(598);
 
    // 驗證資料庫狀態
    const orderCount = await prisma.order.count();
    expect(orderCount).toBe(1);
  });
 
  it("returns 404 for non-existent product", async () => {
    const response = await request(app)
      .post("/api/orders")
      .send({ productId: 999, quantity: 1 });
 
    expect(response.status).toBe(404);
  });
});

Python(pytest + FastAPI TestClient)

python
import pytest
from fastapi.testclient import TestClient
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
 
# 使用 SQLite In-Memory 做整合測試
engine = create_engine("sqlite:///:memory:")
TestSession = sessionmaker(bind=engine)
 
@pytest.fixture
def client(db_session):
    app.dependency_overrides[get_db] = lambda: db_session
    return TestClient(app)
 
@pytest.fixture
def db_session():
    Base.metadata.create_all(bind=engine)
    session = TestSession()
    yield session
    session.close()
    Base.metadata.drop_all(bind=engine)
 
def test_create_order_valid_product(client, db_session):
    # Arrange
    db_session.add(Product(id=1, name="Book", price=299))
    db_session.commit()
 
    # Act
    response = client.post("/api/orders", json={
        "product_id": 1,
        "quantity": 2
    })
 
    # Assert
    assert response.status_code == 201
    assert response.json()["total"] == 598
    assert db_session.query(Order).count() == 1

概念圖

整合測試
測試 HTTP 端點
API 層
呼叫商業邏輯
Service 層
讀寫資料
資料庫
Testcontainers
提供真實容器
In-Memory DB
快速替代方案
WebApplicationFactory

此圖展示整合測試的典型架構:從 API 層到 Service 層到資料庫,形成一條完整的呼叫鏈。三種環境準備工具(Testcontainers、In-Memory DB、WebApplicationFactory)負責提供可控的依賴環境。選擇依據:追求行為精確用 Testcontainers,追求速度用 In-Memory DB。

測試資料隔離策略

| 策略 | 做法 | 速度 | 適用場景 | |------|------|------|----------| | Transaction Rollback | 每個測試包在 Transaction 中,結束時 Rollback | 最快 | 單一資料庫、不測 Transaction 行為 | | 每次重建 DB | 每個測試前 Drop + Create Schema | 慢 | 測試少、Schema 簡單 | | Snapshot Restore | 建好初始資料後拍快照,每次還原 | 中等 | 需要複雜的 Seed Data | | Random Data + Cleanup | 每個測試用唯一 ID,測試後刪除 | 中等 | 共享資料庫、平行執行 |

實戰補充

💡資深開發者的經驗

  • Testcontainers 是最佳實踐:用 Docker 容器跑真實的 PostgreSQL、Redis,行為最接近生產環境。C# 用 Testcontainers.MsSql,JS 用 testcontainers,Python 用 testcontainers-python
  • Transaction Rollback 模式:每個測試用 transaction 包住,結束時 rollback,比每次重建 DB 快很多。
  • CI/CD 中的整合測試:放在 Unit Test 之後、E2E 之前。用 Docker Compose 在 CI 啟動依賴服務。
  • Contract Testing 是整合測試的進階版:用 Pact 等工具驗證 API 契約,不需要啟動所有服務。

理解測驗

🤔 整合測試和單元測試的主要差異是什麼?

🤔 為什麼整合測試需要獨立的測試資料?

🤔 Testcontainers 的主要優勢是什麼?

重點整理

💡一句話記住

口訣:「單元測零件、整合測接縫」—— 零件沒問題不代表裝起來沒問題,接縫才是 Bug 的藏身處。

| 概念 | 說明 | |------|------| | 測試範圍 | 多個元件的互動(API → Service → DB) | | Testcontainers | 用 Docker 容器提供真實的依賴服務 | | In-Memory DB | 快速但行為可能與真實 DB 有差異 | | WebApplicationFactory | ASP.NET Core 內建的整合測試基礎設施 | | 核心好處 | 捕捉元件間的接口問題,投資報酬率高 | | 代價 | 比 Unit Test 慢,需要管理測試環境和資料 |

你可能也想看

測試金字塔參數化測試

按 ← → 鍵切換課程