SCours SwiftUI
Fiche 14.02

Fiche 14.02 — Tester un ViewModel

Objectif

Savoir tester un ViewModel SwiftUI sans lancer l’interface.

C’est une compétence très utile en entretien, car elle montre que ton architecture est propre et testable.

1. ViewModel à tester

Exemple simple de ViewModel de login :

Swift
import Foundation @MainActor final class LoginViewModel: ObservableObject { @Published var email = "" @Published var password = "" @Published var errorMessage: String? var isFormValid: Bool { email.contains("@") && password.count >= 8 } func validate() { if email.isEmpty || password.isEmpty { errorMessage = "Tous les champs sont obligatoires." } else if !isFormValid { errorMessage = "Email ou mot de passe invalide." } else { errorMessage = nil } } }

2. Tester la validation

Swift
import XCTest @testable import MyApp @MainActor final class LoginViewModelTests: XCTestCase { func testEmptyFieldsAreInvalid() { let viewModel = LoginViewModel() viewModel.email = "" viewModel.password = "" viewModel.validate() XCTAssertFalse(viewModel.isFormValid) XCTAssertEqual(viewModel.errorMessage, "Tous les champs sont obligatoires.") } func testValidFormHasNoError() { let viewModel = LoginViewModel() viewModel.email = "user@test.com" viewModel.password = "password123" viewModel.validate() XCTAssertTrue(viewModel.isFormValid) XCTAssertNil(viewModel.errorMessage) } }

@MainActor est utile si ton ViewModel est aussi @MainActor.

3. Tester les états d’écran

Un ViewModel réel expose souvent un état.

Swift
enum ViewState: Equatable { case idle case loading case loaded case error(String) }

ViewModel :

Swift
@MainActor final class ProfileViewModel: ObservableObject { @Published private(set) var state: ViewState = .idle func loadProfile() { state = .loading state = .loaded } }

Test :

Swift
@MainActor final class ProfileViewModelTests: XCTestCase { func testLoadProfileEndsInLoadedState() { let viewModel = ProfileViewModel() viewModel.loadProfile() XCTAssertEqual(viewModel.state, .loaded) } }

4. Tester avec un service mock

ViewModel :

Swift
protocol AuthServicing { func login(email: String, password: String) async throws } @MainActor final class LoginAsyncViewModel: ObservableObject { @Published private(set) var isLoading = false @Published private(set) var errorMessage: String? private let authService: AuthServicing init(authService: AuthServicing) { self.authService = authService } func login(email: String, password: String) async { isLoading = true errorMessage = nil do { try await authService.login(email: email, password: password) } catch { errorMessage = "Connexion impossible." } isLoading = false } }

Mock :

Swift
struct AuthServiceMock: AuthServicing { let shouldFail: Bool func login(email: String, password: String) async throws { if shouldFail { throw URLError(.badServerResponse) } } }

Test :

Swift
@MainActor final class LoginAsyncViewModelTests: XCTestCase { func testLoginFailureShowsError() async { let viewModel = LoginAsyncViewModel( authService: AuthServiceMock(shouldFail: true) ) await viewModel.login(email: "user@test.com", password: "password123") XCTAssertFalse(viewModel.isLoading) XCTAssertEqual(viewModel.errorMessage, "Connexion impossible.") } }

Résumé

  • Un ViewModel se teste sans afficher l’UI.
  • On teste les champs calculés, les erreurs et les états.
  • Les dépendances doivent être injectées pour pouvoir utiliser des mocks.
  • Les tests de ViewModel prouvent que ton MVVM est propre.