Unit Testing MVVM, SwiftUI, Async/Await, and Combine: The Ultimate Quality Assurance Squad 🕵🏿

Di Nerd 🧑🏿‍💻
3 min readMar 20, 2023

--

Greetings, future testing masters! In this article, we’ll uncover the secrets of unit testing our fantastic SwiftUI app, which we built with MVVM, Async/Await, and Combine. It’s like a superhero team-up, but this time with a focus on ensuring our app’s quality and reliability. So, let’s embark on a fun-filled journey to learn, laugh, and create a bulletproof app! 😄

The Testing Realm: XCTest 🧪

XCTest is Apple’s testing framework, and it’s the key to unlocking a world of unit tests for our app. XCTest helps us verify if our app behaves as expected under various conditions, and it’s the guardian of our code’s integrity.

ViewModel Testing: The MVP 🏆

Our ViewModel is the cornerstone of the MVVM architecture. Ensuring that it works correctly is crucial for the success of our app. Let’s create a test case for our ContentViewModel.

First, create a new Unit Test Case class called ContentViewModelTests:

import XCTest
import Combine
@testable import YourAppName

class ContentViewModelTests: XCTestCase {

}

Don’t forget to replace YourAppName with your actual app's name.

Now, let’s write a test to check if our fetchData function fetches and decodes data successfully. We'll use Combine expectations to handle the asynchronous nature of our API call.

func testFetchData() {
// Given
let viewModel = ContentViewModel()
let expectation = expectation(description: "Data fetched and decoded successfully")

// When
viewModel.fetchData()

// Then
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
if let data = viewModel.data {
XCTAssertNotNil(data, "Data should not be nil")
expectation.fulfill()
} else {
XCTFail("Failed to fetch data")
}
}

waitForExpectations(timeout: 5)
}

In this test, we create a ContentViewModel instance and call fetchData. Then, we wait for a short period (3 seconds in this case) to give our app time to fetch and decode the data. If the data is not nil, we consider the test successful.

To make our tests more robust, we can use dependency injection and mock API responses. This way, we can test our ViewModel without relying on real API calls.

Mocking API Calls: The Art of Deception 🎭

Let’s create a protocol called DataFetcher that will help us inject our API calls into our ViewModel:

protocol DataFetcher {
func fetchData() -> AnyPublisher<DataType, Error>
}

Now, update our ContentViewModel to accept an object conforming to DataFetcher:

class ContentViewModel: ObservableObject {
@Published var data: DataType?

private var cancellable: AnyCancellable?
private let dataFetcher: DataFetcher

init(dataFetcher: DataFetcher) {
self.dataFetcher = dataFetcher
}

// ... rest of the code
}

Update the fetchData function to use the injected dataFetcher:

func fetchData() {
cancellable = dataFetcher.fetchData()
.receive(on: DispatchQueue.main)
.sink { completion in
switch completion {
case .failure(let error):
print("Error fetching data: \(error)")
case .finished:
print("Data fetched successfully")
}
} receiveValue: { data in
self.data = data
}
}

Now, let’s create a mock implementation of the DataFetcher protocol. This implementation will return a predefined response without making any actual API calls.

class MockDataFetcher: DataFetcher {
func fetchData() -> AnyPublisher<DataType, Error> {
let data = DataType(id: 1, userId: 1, title: "Mock Title", body: "Mock Body")
return Just(data)
.setFailureType(to: Error.self)
.eraseToAnyPublisher()
}
}

Our MockDataFetcher returns a predefined DataType object when its fetchData function is called.

Now that we have our mock data fetcher, let’s update the ContentViewModelTests to use it:

func testFetchDataWithMock() {
// Given
let mockDataFetcher = MockDataFetcher()
let viewModel = ContentViewModel(dataFetcher: mockDataFetcher)
let expectation = expectation(description: "Data fetched and decoded successfully")

// When
viewModel.fetchData()

// Then
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
if let data = viewModel.data {
XCTAssertNotNil(data, "Data should not be nil")
XCTAssertEqual(data.title, "Mock Title", "The title should match the mock title")
XCTAssertEqual(data.body, "Mock Body", "The body should match the mock body")
expectation.fulfill()
} else {
XCTFail("Failed to fetch data")
}
}

waitForExpectations(timeout: 3)
}

In this test, we use MockDataFetcher instead of making actual API calls. This approach allows us to control the data returned by the fetcher and ensures our tests are more reliable and fast.

In Conclusion: The Quality Assurance Squad in Action 🌟

We’ve successfully built a unit testing suite for our SwiftUI app using MVVM, Async/Await, and Combine. This powerful combination ensures our app’s quality and reliability, providing a fantastic user experience.

So, give yourself a pat on the back, and go ahead, share the knowledge about this ultimate testing squad. Together, let’s create robust and reliable apps and have some fun along the way! 🚀

Happy testing! 😄

--

--