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 中仅需数秒
  • 隔离性强——外部依赖通过 mock/stub 加以控制
  • 可靠性高——出现不稳定测试(flaky test)的风险极低(不涉及网络、数据库)
  • 能够即时捕捉小范围代码变更引入的回归问题
  • 具备文档作用——写得好的单元测试能展示代码该如何使用
  • 配合 TDD 能提升设计质量
  • 可并行运行——能高效利用多核 CPU

缺点

  • Mock 可能无法完全反映真实情况——存在假阳性/假阴性风险
  • 无法发现集成层面的问题——常出现"单元测试全过但生产环境崩溃"的情况
  • 存在过度 mock 的风险——测试可能与实现细节绑定过紧
  • 测试私有方法有时需要改动设计
  • 无法验证彼此独立的模块在真实环境中的协同表现

最适合

业务逻辑验证纯函数与算法测试ViewModel/Reducer 状态迁移测试解析器、格式化器、校验器测试库与框架开发

Integration Test

优点

  • 验证真实的组件交互——不存在 mock 带来的误判
  • 能够发现集成层面的问题——测试各单元协同工作的表现
  • 可以对数据库、网络层进行真实环境测试
  • 端到端验证关键流程
  • 为重构提供保障——能捕捉接口变更引发的问题
  • 更贴近真实的用户使用场景

缺点

  • 运行速度较慢——网络、数据库、文件系统均为实时操作
  • 存在不稳定测试(flaky test)的风险——外部服务可能不可靠
  • 环境搭建与清理(teardown)较为复杂
  • 定位失败测试的根因较为困难
  • 并行运行可能引发状态冲突
  • 需要额外的基础设施来搭建非 mock 环境

最适合

数据库 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/端到端测试(10%)。单元测试用于获得快速反馈,集成测试用于验证关键流程,二者应结合使用。不要用其中一种替代另一种,而应将其作为互补策略来运用。

获取免费咨询
常见问题

常见问题

没有统一的数字标准。80% 以上的业务逻辑覆盖率是一个不错的目标。但比覆盖率数字更重要的是测试的内容是否正确,一味盲目追求 100% 覆盖率反而有害。

相关博客文章

查看全部文章

相关项目

查看全部项目
全部对比