Unit Test

Unit Test & SwiftUI

Mesut Aygun
7 min readDec 1, 2023

--

Using unit tests in SwiftUI is crucial for enhancing the reliability of your application, detecting errors early, and ensuring the sustainability of your code. Here are the advantages and key points to consider when using unit tests in SwiftUI applications.

Advantages:

  1. Improved Reliability:
  • Unit tests contribute to a more reliable application by identifying and addressing errors at an early stage.

2. Early Error Detection:

  • The early detection of errors is facilitated, enabling swift resolution and reducing the likelihood of issues in the development process.

3. Code Sustainability:

  • Unit tests play a significant role in maintaining the sustainability of your codebase, making it easier to manage and evolve over time.

Considerations:

  1. Independence:
  • It is crucial for unit tests to be independent of each other. The failure of one test should not impact the success of others.

2. Speed:

  • Unit tests should execute quickly. Fast tests accelerate the development process and allow for frequent execution.

3. Proper Scope:

  • Ensure that unit tests cover the appropriate scope for the unit under test. Limit interactions outside the relevant unit as much as possible to isolate the tests.

4. Database and External Resources:

  • Unit tests generally should not include external dependencies such as database operations. If your code relies on external resources, use mock objects or test doubles to appropriately simulate these dependencies.

5. Continuous Integration:

  • Integrate unit tests into your continuous integration processes to automatically test your code with each update.

Let’s start writing example tests from simple to complex to better understand unit testing.

  1. Open the “File” menu located in the top-left corner of Xcode.
  2. Select the “New” option.
  3. Choose “Target…” from the dropdown menu.
  4. In the opened window, find and select “iOS Unit Testing Bundle” .
  5. Click “Next” and then “Finish” to add the testing bundle to your Xcode project.

We’re preparing a view model and a view to test a simple boolean value in our application.

class UnitTest_ViewModel : ObservableObject {

@Published var testBoolenValue : Bool

init(testBoolenValue : Bool){
self.testBoolenValue = testBoolenValue
}
}

struct UnitTest_View: View {
@StateObject var vm : UnitTest_ViewModel

init(testBoolenValue : Bool) {
_vm = StateObject(wrappedValue: UnitTest_ViewModel(testBoolenValue: testBoolenValue))
}
var body: some View {
VStack {

Text(vm.testBoolenValue.description)

}
.padding()
}
}
 func test_UnitTest_testBoolenValue_ShouldBeTrue() throws {

// Given

This step signifies the initiation of the test scenario.
In other words, it is where the necessary conditions for
the test to run are established or triggered.

// When

This step specifies the conditions given before the test begins.
It defines the initial state of the test.

//Then

This step expresses the expected outcomes of the test.
The test scenario is verified here, and it is used to check the
accuracy of a specific condition or state."

}

Let’s create our test function inside the test file. Here, we need to observe that the testBoolenValue returns true or false.

   func test_UnitTest_testBoolenValue_ShouldBeTrue() throws {

// Given

let boolenValue : Bool = true

// When

let vm = UnitTest_ViewModel(testBoolenValue: boolenValue)

//Then

XCTAssertTrue(vm.testBoolenValue)
}

func test_UnitTest_testBoolenValue_ShouldBeFalse() throws {

// Given

let boolenValue : Bool = false

// When

let vm = UnitTest_ViewModel(testBoolenValue: boolenValue)

//Then

XCTAssertFalse(vm.testBoolenValue)
}

As seen here, if the test is successful, a green checkmark appears; otherwise, a red cross sign is displayed.

Let’s now perform a stress test on this test process by running it 100 times and verify if we consistently achieve successful results.

Now let’s look at some testing procedures related to arrays. We create dataArray in UnitTest_ViewModel and some test about dataArray object.

class UnitTest_ViewModel : ObservableObject {

@Published var testBoolenValue : Bool

@Published var dataArray : [String] = []

init(testBoolenValue : Bool){
self.testBoolenValue = testBoolenValue
}
}

Now let’s simply test whether this array is empty or not.

Now, let’s add an element to the data array with the function we created in the view model and test it that way.

class UnitTest_ViewModel : ObservableObject {

@Published var testBoolenValue : Bool

@Published var dataArray : [String] = []

init(testBoolenValue : Bool){
self.testBoolenValue = testBoolenValue
}

func addItemDataArray(item : String){
self.dataArray.append(item)
}
}

We write test and check dataArray ;

As you can see, when we added an element to the dataArray, our test did not pass, we observed the situation we expected.

If the string entered by the user already exists in the dataArray, we will try to capture this process and test it.

class UnitTest_ViewModel : ObservableObject {

@Published var testBoolenValue : Bool

@Published var dataArray : [String] = []

@Published var selectedItem : String? = nil
init(testBoolenValue : Bool){
self.testBoolenValue = testBoolenValue
}

func addItemDataArray(item : String){
self.dataArray.append(item)
}

func selectItem(item : String){
if let x = dataArray.first(where: {$0 == item}){
self.selectedItem = x
}
}
}

Given: A model, UnitTest_ViewModel, is created with a property named testBoolenValue that takes a random boolean value. This represents the pre-defined state necessary before the test begins.

When: A new piece of data is generated, which is the string representation of a UUID. This new data is added to the array using the addItemDataArray function of the created view model. Then, the selectItem function is used to make this new data selected.

Then: It is checked using XCTAssertNotNil that selectedItem should not be nil. In other words, if an item is selected, selectedItem should not be nil. It is checked using XCTAssertEqual that selectedItem should be equal to the newly added data. In other words, the selected item should be the same as the added data.

protocol DataServiceProtocol {

func downloadItemsWithEscaping(completion : @escaping (_ items : [String]) -> ())
}
class MockDataService : DataServiceProtocol {

let items : [String]

init(items: [String]?) {
self.items = items ?? [

"ONE","TWO","THREE"
]
}
func downloadItemsWithEscaping(completion : @escaping (_ items : [String]) -> ()){
DispatchQueue.main.asyncAfter(deadline: .now() + 3){
completion(self.items)
}
}


}

The downloadItemsWithEscaping function is designed to download data using a closure. The MockDataService class implements the DataServiceProtocol, including the implementation of the downloadItemsWithEscaping function. It also has a property named items, which represents an array owned by instances of the class. This array takes a specified value when the class is initialized, or defaults to predefined values if not specified.

The downloadItemsWithEscaping function contains an asynchronous task scheduled to run on the main queue after a specific duration, in this case, 3 seconds. This task is configured to complete with data using the provided completion closure. In summary, the class simulates asynchronous data retrieval, and the function completion is deferred until the specified time has passed.

We created downloadWithEscaping function in UnitTest_ViewModel and this function :

    func downloadWithEscaping(){
self.dataService.downloadItemsWithEscaping { returnedItems in

self.dataArray = returnedItems

}
}

Its function is to save the captured data into dataArray.

The returning response values ​​in our asynchronous running function that we will test here ;

The test did not pass successfully because we wanted it to run after 3 seconds in my function. Now its we will update code in again create test.

  1. Given:
  • An instance of UnitTest_ViewModel is created and initialized with a randomly generated boolean value.

2. When:

  • An XCTestExpectation named expectation is created. This expectation is defined as "Data should be returned after 3 seconds."
  • The downloadWithEscaping function is called. This function simulates an asynchronous data download through the MockDataService.

3. Then:

  • Using DispatchQueue.main.asyncAfter, the expectation is fulfilled after waiting for 5 seconds. This simulates waiting for data to be downloaded asynchronously.
  • The wait(for:timeout:) function is used to wait for the specified expectation (data download) for a maximum of 10 seconds.
  • XCTAssertGreaterThan is used to check that the count of items in vm.dataArray is greater than 0.

In this article, we’ve delved into the fundamentals of writing Unit Tests in Swift. Unit tests serve as a powerful tool in the software development lifecycle, enhancing code quality, ensuring reliability, and detecting errors early on. When writing tests, it’s crucial to focus on testing the expected behaviors of functions and handling edge cases, particularly using Mock objects to control external dependencies. Regularly incorporating testing into your application development process will streamline maintenance, making your development cycle faster and more robust.

I hope this article has instilled confidence and knowledge in writing Unit Tests in Swift. Wishing you successful tests and reliable code! 🚀✨

--

--