[TDD] Writing Unit Tests for a ViewModel

Muhammad Alfiansyah
3 min readFeb 7, 2025

--

Let’s say we have a UserViewModel that interacts with AuthManager and SessionManager. Here's a simple example of the ViewModel that you want to test:

final class UserViewModel: ObservableObject {
@Published var isUserSignIn: Bool = false
@Published var user: User?

private let authManager: AuthManagerProtocol
private let sessionManager: SessionManagerProtocol

init(authManager: AuthManagerProtocol, sessionManager: SessionManagerProtocol) {
self.authManager = authManager
self.sessionManager = sessionManager

self.loadUser()
}

func loadUser() {
if let user = sessionManager.loadUser() {
self.user = user
self.isUserSignIn = true
}
}

func signIn() {
authManager.signIn { [weak self] user in
guard let user = user else {
return
}
self?.sessionManager.saveUser(user)
self?.user = user
self?.isUserSignIn = true
}
}

func signOut() {
authManager.signOut()
sessionManager.clearUser()
user = nil
isUserSignIn = false
}
}

To test the ViewModel, you need to mock the AuthManager and SessionManager behaviors. This allows you to isolate and test the ViewModel's logic without calling real APIs or writing to disk.

// Mock for AuthManager
class MockAuthManager: AuthManager {
var signInResult: Bool = true

override func signIn(callback: @escaping (Bool) -> Void) {
callback(signInResult)
}

override func signOut() {
// Simulate sign-out, no need for further logic
}
}

// Mock for SessionManager
class MockSessionManager: SessionManager {
var mockUser: User?

override func saveUser(_ user: User) {
mockUser = user
}

override func loadUser() -> User? {
return mockUser
}

override func clearUser() {
mockUser = nil
}
}

Now, let’s write unit tests for UserViewModel using XCTest. The goal is to test the behavior of UserViewModel during sign-in and sign-out, ensuring it correctly updates the isUserSignedIn and currentUser properties.

import XCTest
@testable import YourApp

class UserViewModelTests: XCTestCase {
var viewModel: UserViewModel!
var mockAuthManager: MockAuthManager!
var mockSessionManager: MockSessionManager!

override func setUp() {
super.setUp()
mockAuthManager = MockAuthManager()
mockSessionManager = MockSessionManager()
viewModel = UserViewModel(
authManager: mockAuthManager,
sessionManager: mockSessionManager
)
}

func testUserSignedIn() {
let user = User(id: 123, name: "John Doe", email: "john@example.com")
mockSessionManager.saveUser(user)

viewModel.loadUser()

XCTAssertTrue(viewModel.isUserSignIn, "User should be sign in")
XCTAssertEqual(viewModel.user?.id, user.id, "User should match")
}

func testSignInSuccess() {
let user = User(id: 123, name: "John Doe", email: "john@example.com")
mockAuthManager.signInUser = user

let expectation = self.expectation(description: "Sign in should complete")
viewModel.signIn()

DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
XCTAssertTrue(self.viewModel.isUserSignIn, "User should be sign in")
XCTAssertNotNil(self.viewModel.user, "User should be present")
expectation.fulfill()
}

wait(for: [expectation], timeout: 1.0)
}

func testSignInFailed() {
let expectation = self.expectation(description: "Sign in should fail")
viewModel.signIn()

DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
XCTAssertFalse(self.viewModel.isUserSignIn, "User should not be sign in")
XCTAssertNil(self.viewModel.user, "User should be nil")
expectation.fulfill()
}

wait(for: [expectation], timeout: 1.0)
}

func testSignOut() {
let user = User(id: 123, name: "John Doe", email: "john@example.com")
mockSessionManager.saveUser(user)

viewModel.loadUser()

viewModel.signOut()

XCTAssertFalse(viewModel.isUserSignIn, "User should be sign out")
XCTAssertNil(viewModel.user, "User should be nil")
}
}

class MockAuthManager: AuthManagerProtocol {
var signInUser: User?

func signIn(callback: @escaping (User?) -> Void) {
callback(signInUser)
}

func signOut() {
signInUser = nil
}
}

class MockSessionManager: SessionManagerProtocol {
var signInUser: User?

func saveUser(_ user: User) {
signInUser = user
}

func loadUser() -> User? {
return signInUser
}

func clearUser() {
signInUser = nil
}
}
  • Mocking dependencies (e.g., AuthManager, SessionManager) ensures that the ViewModel is isolated and only tested for its logic.
  • Asynchronous behavior is handled using XCTestExpectation, allowing us to wait for the signIn method to complete before validating the results.
  • Edge cases like sign-in failures are tested to ensure the ViewModel reacts correctly.

--

--

No responses yet