Skip to content

hmbz/iOS-Unit-Testing-Demo

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

1 Commit
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

iOS Unit Testing Demo — XCTest + async/await

A complete guide to unit testing in iOS — testing ViewModels, Use Cases, and the Network layer using XCTest, Mocks, and async/await.


What is Unit Testing?

A unit test verifies that one small piece of logic works correctly in complete isolation — no real network, no real database, no UI.

Without tests:             With tests:
"I think it works" ❓      "I know it works" ✅
Manual testing every time  Automated — runs in seconds
Fear of changing code      Confidence to refactor freely

What is a Mock?

A Mock is a fake version of a dependency used only in tests.

// Real — hits the network
class UserRepository: UserRepositoryProtocol {
    func fetchUsers() async throws -> [User] {
        // real API call...
    }
}

// Mock — returns fake data instantly
class MockUserRepository: UserRepositoryProtocol {
    var usersToReturn: [User] = []
    var errorToThrow: Error? = nil

    func fetchUsers() async throws -> [User] {
        if let error = errorToThrow { throw error }
        return usersToReturn   // instant, predictable, no network
    }
}

Test Structure — AAA Pattern

Every test follows the Arrange → Act → Assert pattern:

func test_loadUsers_onSuccess_populatesUsersArray() async {
    // ARRANGE — set up the scenario
    mockRepository.usersToReturn = [
        User(id: 1, name: "Ali", email: "ali@test.com")
    ]

    // ACT — run the function under test
    await sut.loadUsers()

    // ASSERT — verify the outcome
    XCTAssertEqual(sut.users.count, 1)
    XCTAssertNil(sut.errorMessage)
    XCTAssertFalse(sut.isLoading)
}

Tests Included

UseCase Tests — FetchUsersUseCaseTests

Test Verifies
test_execute_withValidUsers_returnsSortedUsers Business rule: sorted A→Z
test_execute_withEmptyEmailUser_filtersThemOut Business rule: no empty emails
test_execute_withEmptyList_returnsEmptyArray Edge case: empty result
test_execute_whenRepositoryThrows_propagatesError Error propagation
test_execute_callsRepositoryExactlyOnce No duplicate calls
test_execute_withAllEmptyEmails_returnsEmptyArray All filtered out

ViewModel Tests — UsersViewModelTests

Test Verifies
test_loadUsers_onSuccess_populatesUsersArray Happy path
test_loadUsers_onSuccess_clearsErrorMessage Error cleared on retry
test_loadUsers_withEmptyResult_hasEmptyUsersArray Empty state
test_loadUsers_isLoadingFalse_afterCompletion Loading resets
test_loadUsers_onNoInternet_setsErrorMessage Network error UI
test_loadUsers_onUnauthorized_setsCorrectError 401 error message
test_loadUsers_isLoadingFalse_evenAfterError Loading resets on failure

Network Tests — NetworkClientTests

Test Verifies
test_request_withValidJSON_decodesCorrectly JSON decoding
test_request_withUsersListJSON_decodesAllUsers Array decoding
test_request_withEmptyArray_returnsEmptyList Empty array
test_request_withInvalidJSON_throwsDecodingError Malformed JSON
test_request_whenUnauthorized_throwsUnauthorizedError 401 error
test_request_isCalledExactlyOnce Call count tracking

Project Structure

iOSUnitTesting/
├── Source/                          ← Production code
│   ├── Models/
│   │   └── User.swift
│   ├── Network/
│   │   ├── NetworkError.swift
│   │   └── NetworkClient.swift
│   ├── Repository/
│   │   ├── UserRepositoryProtocol.swift
│   │   └── UserRepository.swift
│   ├── UseCases/
│   │   └── FetchUsersUseCase.swift
│   └── ViewModel/
│       └── UsersViewModel.swift
│
└── Tests/                           ← Test code
    ├── Mocks/
    │   ├── MockUserRepository.swift  ← Fake repository
    │   └── MockNetworkClient.swift   ← Fake network + test data
    ├── UseCaseTests/
    │   └── FetchUsersUseCaseTests.swift
    ├── ViewModelTests/
    │   └── UsersViewModelTests.swift
    └── NetworkTests/
        └── NetworkClientTests.swift

How to Run Tests

Xcode → Product → Test   (⌘ + U)

All tests should pass with green checkmarks ✅


Key Principles

Principle Description
Test in isolation Each test focuses on one class only
No real network Mocks replace all external dependencies
AAA Pattern Arrange → Act → Assert, every test
One assertion per test Clear failure messages
Protocol-based DI Makes mocking possible

Requirements

  • iOS 17.0+
  • Xcode 15.0+
  • Swift 5.9+

Author

Bilal Zafar — iOS Developer
GitHubLinkedIn

About

iOS Unit Testing with XCTest — ViewModel, UseCase, and Network layer tests using Mocks and async/await

Topics

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages