FI
ForgeInject

Testing

Two approaches: constructor injection (preferred) or container-based mocking.

Creating Mocks

Create mock classes that conform to the same protocols you registered. Control their behavior with simple properties.

final class MockNetworkService: NetworkServiceProtocol {
    var fetchResult: Result<Data, Error> = .success(Data())

    func fetch(_ endpoint: String) async throws -> Data {
        try fetchResult.get()
    }
}

final class MockUserRepository: UserRepositoryProtocol {
    var stubbedUser: User?

    func fetchCurrentUser() async throws -> User {
        guard let user = stubbedUser else { throw MockError.notFound }
        return user
    }
}

Example Subject

A typical @Injectable view model that depends on two services:

@Injectable
final class ProfileViewModel {
    let networkService: NetworkServiceProtocol
    let repository: UserRepositoryProtocol

    private(set) var state: LoadState = .idle

    func loadProfile() async {
        state = .loading
        do {
            let user = try await repository.fetchCurrentUser()
            let data = try await networkService.fetch("/profile/\(user.id)")
            state = .loaded
        } catch {
            state = .failed(error)
        }
    }
}

Approach 1: Constructor Injection (preferred)

Because @Injectable generates a matching init, tests pass mocks directly as arguments — no container, no global state, no setup/teardown.

Why this is preferred: no shared state between tests, no race conditions on ForgeContainer.shared, and the test clearly shows which dependencies the subject actually uses.

Approach 2: Container-Based Mocking

When you can't use constructor injection — typically because the subject uses @Inject or #inject() instead of @Injectable — set up a test container and point ForgeContainer.shared at it.

Important: ForgeContainer.shared enforces set-once-per-app semantics via precondition. In tests, this means you must either (a) mark suites touching shared with .serialized or (b) clear shared to nil in teardown before the next test sets it.

Best Practices

  • Prefer @Injectable for new code — constructor injection gives you the cleanest test story with zero container gymnastics.
  • Serialize tests that touch ForgeContainer.shared — use @Suite(.serialized) (Swift Testing) or one-at-a-time execution (XCTest).
  • Always clear shared = nil in teardown — otherwise the next test that tries to set it will crash with the set-once precondition.
  • Mock protocols, not concrete classes — if you catch yourself mocking a concrete class, register it as a protocol instead.