Unit Test vs Integration Test Comparison

Speed, isolation, and a reliable feedback loop

VS
Integration Test

Real component interaction, end-to-end verification

8 min readiOS

Quick Verdict

The testing pyramid: lots of unit tests (70%), fewer integration tests (20%), a minimum of UI/E2E tests (10%). Use unit tests for fast feedback and integration tests to validate critical flows — together. The right strategy is to use them as complements, not substitutes for one another.

Unit TestIntegration Test
Read the full verdict

Score Comparison

Loading chart...

Detailed Scoring

Detailed Scoring: Unit Test and Integration Test — category-by-category scores out of 10
CategoryUnit TestIntegration Test
Performance
10/10
4/10
Ease of Learning
8/10
6/10
Ecosystem
9/10
8/10
Community
9/10
8/10
Job Market
9/10
8/10
Future-Proof
9/10
9/10

Pros & Cons

Unit Test

Pros

  • Extremely fast — runs in milliseconds, seconds in CI
  • Isolated — external dependencies are controlled via mocks/stubs
  • Reliable — minimal risk of flaky tests (no network, no DB)
  • Regression detection — catches small code changes instantly
  • Documentation — well-written unit tests show how code should be used
  • Raises design quality through TDD
  • Can run in parallel — makes efficient use of multiple cores

Cons

  • Mocks may not fully reflect reality — risk of false positives/negatives
  • Can't catch integration issues — the classic 'unit tests passed but production broke' scenario
  • Risk of over-mocking — tests can become tightly coupled to implementation
  • Testing private methods may require design changes
  • Doesn't show how independent modules actually behave together in the real world

Best For

Validating business logicTesting pure functions and algorithmsViewModel/Reducer state transitionsParser, formatter, and validator testsLibrary and framework development

Integration Test

Pros

  • Validates real component interaction — no mock illusion
  • Catches integration bugs — tests whether units actually work together
  • Tests database and network layers as they really behave
  • End-to-end verification of critical flows
  • Refactoring confidence — catches interface changes
  • Tests closer to real user scenarios

Cons

  • Slow — real-time network, database, and file-system access
  • Risk of flaky tests — external services can be unreliable
  • Setup and teardown complexity
  • Hard to pinpoint the cause of failures
  • Parallel runs can create state conflicts
  • Requires extra infrastructure beyond a mock-free environment

Best For

Database CRUD operations (Core Data, SwiftData, SQLite)Verifying the API client layerMultiple services working togetherVerifying critical paths (payment, auth flows)Data migration and transformation tests

Code Comparison

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

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

    @Test("Turkish lira formatting should be correct")
    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("Invalid price should not be negative",
          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("Total price should update when a product is added")
    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("Quantity should increase when the same product is added twice")
    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 implementation
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 examples
import XCTest
@testable import MyApp

// Integration test with in-memory SQLite
class UserRepositoryIntegrationTests: XCTestCase {
    var repository: UserRepository!
    var database: TestDatabase!

    override func setUp() async throws {
        // We use a real in-memory SQLite database
        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 Yilmaz",
            email: "[email protected]"
        )

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

        XCTAssertNotNil(fetchedUser)
        XCTAssertEqual(fetchedUser?.name, "Ahmet Yilmaz")
        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)

        // Delete the user
        try await repository.deleteUser(id: userId)

        // The user's posts should be deleted too (cascade)
        let posts = try await postRepo.fetchPosts(userId: userId)
        XCTAssertTrue(posts.isEmpty, "Deleting the user should also delete their posts")
    }
}

// Network integration test with a real URLSession
class APIClientIntegrationTests: XCTestCase {
    func testFetchPublicAPIData() async throws {
        // This test uses a real network connection
        // Should only run in CI environments with network access
        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)
    }
}

Conclusion

The testing pyramid: lots of unit tests (70%), fewer integration tests (20%), a minimum of UI/E2E tests (10%). Use unit tests for fast feedback and integration tests to validate critical flows — together. The right strategy is to use them as complements, not substitutes for one another.

Get Free Consultation
FAQ

Frequently Asked Questions

There's no magic number. 80%+ coverage of business logic is a good target. But testing the right things matters more than the coverage percentage — blindly chasing 100% coverage is counterproductive.

Related Blog Posts

View All Posts

Related Projects

View All Projects
All Comparisons