Unit Test vs Integration Test 比較

速度、分離、信頼できるフィードバックループ

VS
Integration Test

実際のコンポーネント同士のやり取り、エンドツーエンドの検証

8 分で読了iOS

スコア比較

グラフを読み込み中...

詳細スコア

詳細スコア: Unit Test Integration Test — カテゴリー別10点満点のスコア
カテゴリーUnit TestIntegration Test
パフォーマンス
10/10
4/10
学習のしやすさ
8/10
6/10
エコシステム
9/10
8/10
コミュニティ
9/10
8/10
求人市場
9/10
8/10
将来性
9/10
9/10

長所と短所

Unit Test

長所

  • 非常に高速 — ミリ秒単位で実行、CIでは数秒
  • 分離されている — 外部依存はモック/スタブで制御
  • 信頼性が高い — flakyテストのリスクが最小(ネットワーク・DBなし)
  • 回帰検出 — わずかなコード変更を即座に検知
  • ドキュメント — 良く書かれたユニットテストはコードの使い方を示す
  • TDDにより設計品質を向上
  • 並列実行可能 — マルチコアを効率的に活用

短所

  • モックは現実を完全に反映しない場合がある — 偽陽性/偽陰性のリスク
  • 統合上の問題を検出できない — 「ユニットテストは通ったが本番で壊れた」というシナリオが頻発
  • 過剰モックの危険性 — テストが実装に強く結びつく可能性
  • プライベートメソッドをテストするには設計変更が必要な場合がある
  • 独立したモジュール同士の実世界での整合性を示さない

最適な用途

ビジネスロジックの検証純粋な関数やアルゴリズムのテストViewModel/Reducerの状態遷移パーサー、フォーマッター、バリデーターのテストライブラリ・フレームワーク開発

Integration Test

長所

  • 実際のコンポーネント間のやり取りを検証 — モックによる誤解がない
  • 統合上のエラーを検出 — ユニット同士が正しく連携するかテスト
  • データベース、ネットワーク層を実環境に近い形でテスト可能
  • 重要なフローをエンドツーエンドで検証
  • リファクタリングの安全性 — インターフェース変更を検知
  • ユーザーシナリオに近いテスト

短所

  • 遅い — ネットワーク、データベース、ファイルシステムがリアルタイムで動作
  • flakyテストのリスク — 外部サービスが不安定な場合がある
  • セットアップとティアダウンの複雑さ
  • 失敗したテストの原因特定が困難
  • 並列実行がstateの競合を引き起こす可能性
  • モック環境ではないため追加のインフラが必要

最適な用途

データベースのCRUD操作(Core Data、SwiftData、SQLite)APIクライアント層の検証複数サービス連携の動作確認クリティカルパス(決済、認証フロー)の検証データマイグレーション・変換のテスト

コード比較

Unit Test
// Swift - Unit test örnekleri (XCTest + Swift Testing)
import Testing
import Foundation
@testable import MyApp

// Swift Testing framework (iOS 17+, WWDC 2024)
struct PriceFormatterTests {

    @Test("Türk lirası formatlaması doğru olmalı")
    func turkishLiraFormat() {
        let formatter = PriceFormatter(locale: Locale(identifier: "tr_TR"))
        #expect(formatter.format(1234.5) == "₺1.234,50")
        #expect(formatter.format(0) == "₺0,00")
        #expect(formatter.format(-50) == "-₺50,00")
    }

    @Test("Geçersiz fiyat negatif olmamalı",
          arguments: [-1.0, -100.0, -0.01])
    func negativePriceValidation(price: Double) {
        let validator = PriceValidator()
        #expect(!validator.isValid(price))
    }
}

struct CartViewModelTests {
    var sut: CartViewModel!
    var mockRepository: MockCartRepository!

    @Test("Ürün eklenince toplam fiyat güncellenmeli")
    mutating func addProductUpdatesTotalPrice() async throws {
        mockRepository = MockCartRepository()
        sut = CartViewModel(repository: mockRepository)

        let product = Product(id: "p1", name: "MacBook", price: 75000)
        await sut.addProduct(product)

        #expect(sut.totalPrice == 75000)
        #expect(sut.itemCount == 1)
    }

    @Test("Aynı ürün iki kez eklenince miktar artmalı")
    mutating func addSameProductIncreasesQuantity() async throws {
        mockRepository = MockCartRepository()
        sut = CartViewModel(repository: mockRepository)

        let product = Product(id: "p1", name: "MacBook", price: 75000)
        await sut.addProduct(product)
        await sut.addProduct(product)

        #expect(sut.items.count == 1)
        #expect(sut.items.first?.quantity == 2)
        #expect(sut.totalPrice == 150000)
    }
}

// Mock implementasyonu
class MockCartRepository: CartRepositoryProtocol {
    var savedItems: [CartItem] = []

    func save(_ item: CartItem) async throws {
        savedItems.append(item)
    }

    func fetchAll() async throws -> [CartItem] {
        return savedItems
    }
}
Integration Test
// Swift - Integration test örnekleri
import XCTest
@testable import MyApp

// In-memory SQLite ile integration test
class UserRepositoryIntegrationTests: XCTestCase {
    var repository: UserRepository!
    var database: TestDatabase!

    override func setUp() async throws {
        // Gerçek SQLite in-memory database kullanıyoruz
        database = try await TestDatabase.inMemory()
        repository = UserRepository(database: database)
    }

    override func tearDown() async throws {
        try await database.cleanup()
        database = nil
        repository = nil
    }

    func testCreateAndFetchUser() async throws {
        // Create
        let userId = try await repository.createUser(
            name: "Ahmet Yılmaz",
            email: "[email protected]"
        )

        // Fetch
        let fetchedUser = try await repository.fetchUser(id: userId)

        XCTAssertNotNil(fetchedUser)
        XCTAssertEqual(fetchedUser?.name, "Ahmet Yılmaz")
        XCTAssertEqual(fetchedUser?.email, "[email protected]")
    }

    func testDeleteUserCascadesToPosts() async throws {
        let userId = try await repository.createUser(name: "Test", email: "[email protected]")
        let postRepo = PostRepository(database: database)
        _ = try await postRepo.createPost(title: "Post 1", userId: userId)
        _ = try await postRepo.createPost(title: "Post 2", userId: userId)

        // Kullanıcıyı sil
        try await repository.deleteUser(id: userId)

        // Kullanıcının postları da silinmeli (cascade)
        let posts = try await postRepo.fetchPosts(userId: userId)
        XCTAssertTrue(posts.isEmpty, "Kullanıcı silinince postları da silinmeli")
    }
}

// Gerçek URLSession ile network integration test
class APIClientIntegrationTests: XCTestCase {
    func testFetchPublicAPIData() async throws {
        // Bu test gerçek ağ bağlantısı kullanıyor
        // CI'da sadece network bağlantılı ortamlarda çalıştırılmalı
        try XCTSkipUnless(ProcessInfo.processInfo.environment["INTEGRATION_TESTS"] == "1")

        let client = APIClient(baseURL: URL(string: "https://jsonplaceholder.typicode.com")!)
        let posts: [Post] = try await client.request(path: "/posts")
        XCTAssertFalse(posts.isEmpty)
        XCTAssertEqual(posts.count, 100)
    }
}

結論

テストピラミッド: 多数のユニットテスト(70%)、少数の統合テスト(20%)、最小限のUI/E2Eテスト(10%)。ユニットテストは迅速なフィードバックのため、統合テストは重要なフローの検証のために、両方を併用してください。片方をもう片方の代わりにするのではなく、補完的に使うのが正しい戦略です。

無料相談を受ける
FAQ

よくある質問

魔法の数字はありません。80%以上のビジネスロジックカバレッジは良い目標です。ただし重要なのはカバレッジの割合ではなく、正しいものをテストすることです。盲目的に100%カバレッジを目指すのは有害です。

関連ブログ記事

すべての記事を見る
すべての比較