How would you structure unit tests for a ViewModel (using MVVM) that has dependencies (like a Network Service) using dependency injection and mocking?

iOS interview question for Intermediate practice.

Answer

To structure unit tests for a ViewModel with dependencies, leverage dependency injection and mocking. This isolates the ViewModel's logic from external factors, making tests reliable and independent. Example: Let's assume a UserViewModel fetching user data via a NetworkService: swift protocol NetworkService { func fetchUser(completion: @escaping (Result<User, Error) - Void) } class UserViewModel { private let networkService: NetworkService @Published var user: User? @Published var isLoading: Bool = false @Published var error: Error? init(networkService: NetworkService) { self.networkService = networkService } func fetchUserData() { isLoading = true networkService.fetchUser { [weak self] result in defer { self?.isLoading = false } switch result { case .success(let user): self?.user = user case .failure(let error): self?.error = error } } } } Unit Testing: swift import XCTest @testable import YourProjectName // Replace with your project name class UserViewModelTests: XCTestCase { func testFetchUserDataSuccess() { // Create a mock network service let mockNetworkService = MockNetworkService() mockNetworkService.user = User(id: 1, name: "Test User") // Prepare mock data // Initialize the view model with the mock let viewModel = UserViewModel(networkService: mockNetworkService) // Call the method being tested viewModel.fetchUserData() // Assertions to validate the expected behavior XCTAssertTrue(viewModel.isLoading == false, "Loading indicator should be false after completion") XCTAssertEqual(viewModel.user?.name, "Test User", "User name should match") XCTAssertNil(viewModel.error, "No error should be present") } func testFetchUserDataFailure() { // ... similar setup as above but with MockNetworkService returning failure // Assertions to check error state } } class MockNetworkService: NetworkService { var user: User? var error: Error? func fetchUser(completion: @escaping (Result<User, Error) - Void) { if let user = user { completion(.success(user)) } else if let error = error { completion(.failure(error)) } } } Best Practices: Keep tests small and focused: Each test should cover a specific aspect of the ViewModel's functionality. Use clear and descriptive test names: Test names should clearly indicate what's being tested and the expected outcome. Mock dependencies effectively: Mock services should simulate various scenarios (success, failure, network errors). Test edge cases and error handling: Include tests for exceptional scenarios and ensure proper error handling. Test asynchronous operations: Use XCTestExpectation for asynchronous code testing.

Explanation

In Swift, @testable import allows access to internal structures and methods during testing, crucial for testing ViewModels.

Related Questions