整合測試(Integration Testing)
是什麼?
整合測試(Integration Testing)驗證多個元件、模組或服務之間的互動是否正確。它比單元測試涵蓋更多的真實依賴,但比 E2E 測試範圍更小、速度更快。
ℹ️測試金字塔中的中層
整合測試位於測試金字塔的中間層,是投資報酬率最高的測試類型之一。它能捕捉到單元測試漏掉的互動問題,又比 E2E 快得多。
核心觀念
- 測試真實互動:讓元件使用真實(或接近真實)的依賴,而不是全部 Mock。判斷標準:如果 Mock 掉某個依賴會讓測試失去意義(例如 Mock 掉 SQL 查詢就無法驗證查詢邏輯),就用真實依賴
- 邊界測試:專注於元件之間的接口——API 契約(HTTP 狀態碼、回應格式)、資料庫查詢(SQL 語法、JOIN 邏輯)、訊息格式(JSON 序列化/反序列化)
- Testcontainers:用 Docker 容器啟動真實的資料庫、Redis、Kafka 來跑測試。安裝步驟:C# 裝
Testcontainers.MsSqlNuGet 套件;JS 裝testcontainersnpm 套件;Python 裝testcontainerspip 套件。前提:本機必須有 Docker Desktop 或 Docker Engine 在執行 - In-Memory DB:用 SQLite In-Memory 替代真實資料庫,速度快但行為接近。注意陷阱:EF Core 的 InMemory Provider 不支援 Foreign Key 約束、Transaction、Raw SQL,用 SQLite 的
DataSource=:memory:會更可靠 - WebApplicationFactory:ASP.NET Core 內建的整合測試工具,在測試進程內啟動完整的 HTTP pipeline(包含 Middleware、Routing、DI)。用法:讓測試類別實作
IClassFixture<WebApplicationFactory<Program>>,透過factory.CreateClient()取得 HttpClient
常見誤區
⚠️整合測試的陷阱
- 整合測試不是「大型單元測試」:不要把所有依賴都 Mock 掉,那就只是偽裝成整合測試的單元測試
- 測試資料互相汙染:每個測試必須有獨立的資料,用 transaction rollback 或重建資料庫確保隔離
- 環境依賴:不要依賴外部服務的實際狀態,用 Testcontainers 或 Docker Compose 自帶環境
- 太慢就不跑:整合測試如果跑超過 5 分鐘,團隊就不會常跑,失去價值
實作流程
選擇測試範圍
決定要測哪些元件之間的互動(如 API → Service → DB)
準備測試環境
用 Testcontainers 或 In-Memory DB 啟動依賴
準備測試資料
Seed 必要的初始資料,確保每個測試獨立
執行整合操作
透過 API 或 Service 層發起真實的操作
驗證跨元件結果
檢查資料庫狀態、回應內容、事件是否正確發送
清理環境
還原資料庫或銷毀容器,為下一個測試做準備
程式碼範例
C#(xUnit + WebApplicationFactory)
// 使用 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)
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)
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概念圖
此圖展示整合測試的典型架構:從 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 慢,需要管理測試環境和資料 |