[TDD] Writing Unit Tests for a ViewModel
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 theViewModel
is isolated and only tested for its logic. - Asynchronous behavior is handled using
XCTestExpectation
, allowing us to wait for thesignIn
method to complete before validating the results. - Edge cases like sign-in failures are tested to ensure the
ViewModel
reacts correctly.