Implement Unit Testing in a simple iOS app with MVVM and RxSwift

You probably have heard of “Unit Testing” or already adding unit test in your work now. In summary, it is a way for the developers to safeguard their code against surprising bugs or unwanted results. The term “Unit” indicates the testing is done to test a small chunk of code in the project at one time. It is also independent of any dependencies, we will mock the dependencies, such as third party libraries or classes if there are dependencies in the part of the code we want to add unit test in, which I will show later in the article.

To understand the best practice for unit testing, we will follow the FIRST principle shared by David Piper in his article. They are:

  • Fast: Tests should run quickly.
  • Independent/Isolated: Tests shouldn’t share state with each other.
  • Repeatable: You should obtain the same results every time you run a test. External data providers or concurrency issues could cause intermittent failures.
  • Self-validating: Tests should be fully automated. The output should be either “pass” or “fail”, rather than relying on a programmer’s interpretation of a log file.
  • Timely: Ideally, you should write your tests before writing the production code they test. This is known as test-driven development.

Following these 5 criteria when implementing unit testing into your project should keep your tests neat and clean.

What to test?

Generally, you want to write tests in the general workflow that your app has, core functionality, and bug fixes.

Implementation

I will continue to explain how I add unit testing into the SpaceX Launches app that I have built earlier in my last article, Build a simple SpaceX Launches iOS app with MVVM and RxSwift. You can read up the article to understand more about the project and download the project from GIthub.

When you create any new iOS project and check “Include Tests”, Xcode automatically creates the test target for your project. Else, you can add Unit Testing Bundle by creating a new target.

Create Unit Test Bundle

In SpaceXLaunch project, you can see the test target that Xcode created are SpaceXLaunchTests for Unit Testing and SpaceXLaunchUITests for UI Testing. We will only add the implementation in SpaceXLaunchTests in this article.

Delete the SpaceXLaunchTests.swift created by Xcode. First of all, we will test whether the model files can decode the JSON response given by the API correctly. To do that, we will need to store the sample JSON response and write the test cases for each of the models we have in the project. So, let’s add the sample JSON responses from the 2 APIs to SpaceXLaunchTest project and put them in a folder called Data.

Once you have added the JSON responses, we will start to write unit test for the Models we used in the project: LaunchResponse and LaunchTo write the test cases for each of them, the first thing we need to do is to write the test cases for each of the files, and the naming convention for test case is your filename + Tests, ex: LaunchResponseTests. First of all, lets create a folder called Models, then add new file into the folder and choose Unit Test Case Class.

Create New Unit Test Case

You should see the default test case being created by Xcode like the below.

XCTest is the default Apple framework for creating unit test in iOS. There are many other third party libraries that created on top of it and make it even more intuitive and easier to create tests, such as Quick, Nimble, Appium, etc but we will just be using the default XCTest framework in this article. The test case we created have to inherit from XCTestCase class, else it won’t be recognised as a test case and run the tests in the file.

Xcode will recognize and run each test function if the function name is prefixed with “test”, for example, “testExample“. Everytime when running the test functions, Xcode will first run setUpWithError to set up the objects, then run the test function, finally run the  tearDownWithError function to deinit or destroy any object that we have created. In summary:

  1. setUpWithError – We will set up and intiialise any instance or object we required in this function.

  2. tearDownWithError -We will deinit and destroy the instance or object that setUpWithError has created in this function.

After you have created your test case, you can try to run the unit test by pressing shortcut key, Command + U. Xcode will build the  project and run the unit test. You should be able to see “Test Succeeded” now as we have not written any code in the test functions yet, so Xcode will treat it as a successful test if there is no error being raised when running the function. If you go to the Test Navigator, you should be able to see the test target, test case and test functions that Xcode just run. They will be shown with the green color ticks when the tests are successfully run.

SpaceXLaunchTests Result

Then, we can create 2 helper functions. Let’s create a folder called Helpers and add Data+Extensions.swift and DecodableTestCase.swift into it. Then, copy-paste the code below to each file.

Okay, let’s explain one by one what these helper functions are and how they help us in writing our test cases. In Data+Extensions.swift, we create a helper function in the extension of Data struct to automatically load the JSON from the JSON sample files that we have created earlier. In DecodableTestCase.swift, we created a protocol called DecodableTestCase and added a mixin function to help decode the JSON that we received from Data.fromJSON function to the class that our test case is testing in, SUT is the acronym of System Under Test. These helper functions are from the book, iOS Test-Driven Development by Tutorials | raywenderlich.com. If you are interested in learning Unit-Testing and Test-Driven Development (TDD), I highly recommend you purchase this book to read and learn about this topic.

Disclaimer: I do not get any rewards from Raywenderlich.com if you click the link and purchase the book, I don’t mind if they do introduce the referral program or the affiliate link one day though. 😂

Let’s focus back on the project. Next, we will copy-paste the code below to LaunchResponseTests.swift.

I will explain what each code did below:

  1. SpaceXLaunchTests is a different target than our main project, SpaceXLaunch, so in order for the files to be able to recognise and use any code in the main project, we have to import SpaceXLaunch. @testable allow the file to access all internal entities in the main project without having to explicitly modify them as public , which allows everyone to be able to access it when importing the main project and this is not ideal.
  2. We let LaunchResponseTests class to inherit XCTestCase, DecodableTestCase., so that we can use givenSUTFromJSON() in setUpWithError function later.
  3. We declare a variable named sut to assign the class under test in this file, LaunchResponse. We create it with force-unwrapping as we will only assign value to it in setUpWithError, so the value will never be nil when running the test.
  4. We load and decode the JSON and assign to the sut variable using the helper functions that we have created earlier.
  5. We have to set the sut and other instances that we have created to nil, so that our test function always start with a new state.
  6. We test whether the sut comforms to Decodable.
  7. We test whether the sut comforms to Equatable.
  8. Finally, we test whether the sut has loaded from JSON correctly by comparing it with the expected result.

Assertion

XCTAssertNotNil, XCTAssertEqual and XCTAssertTrue are functions provided by XCTest framework to run assertion on the value we passed in. For example, In XCTAssertTrue((sut as Any) is Decodable), we are telling Xcode to verify whether (sut as Any) is Decodable will return true. If it is true, then Xcode will mark it as a successful test, else it will say that it fails the test. You can learn about other assert functions in Apple’s XCTest documentation.

Now, if you run the test, you should be able to see the functions are shown in the test navigator because Xcode recognises the test functions we just added based on the prefix “test”.

You can try to change assertion logic yourself to purposely make it fail the test and check the test navigator result. Then, we will continue to add LaunchTests into Models folder.

Most of the code in LaunchTests are similar in LaunchResponseTests. One of the key differences is LaunchTests are not inheriting from DecodableTestCase protocol because we want to load the json from LaunchResponse.json which is different name than Launch, the sut, so we are creating a similar helper function in LaunchTests itself instead. Secondly, we have added more functions to compare the loaded value with the result we are expecting.

Next, we want to test the APIService class. Before that, we will need to create some mock files.

Mocks

Mocking is primarily used in unit testing. An object under test may have dependencies on other (complex) objects. To isolate the behavior of the object you want to replace the other objects by mocks that simulate the behavior of the real objects. This is useful if the real objects are impractical to incorporate into the unit test.

In short, mocking is creating objects that simulate the behavior of real objects.

Above is the definition of Mock as noun in the dictionary.

When we implement Unit-Testing, sometimes the class under test will have some dependencies to other frameworks. So in order to follow FIRST principle, we will have to create the mock files to simulate those dependencies, so that we continue to unit test the class under test without any blockage.

Now, let’s create a new folder called Mocks and add these 2 files: MockDataRequest and MockAPIService and copy-paste the code below to the files.

  1. Import third party library for networking purpose.
  2. We create the mock class conforming to DataRequestProtocol, so that later we can use Dependency Injection technique to pass to the other functions or classes.
  3. We create the static variables here, so that we can control and change the value to test different outcomes, example: failure or success scenario.
  1. Same with MockDataRequest and MockSessionManager, we create MockAPIService conforming to APIServiceProtocol to allow Dependency Injection later.
  2. In fetchLaunchesWithQuery, we can control the return value that we want to get, so that we can test the outcome that we want.

You might also notice there is Rocket keyword in the file. There is because the project actually consists of 2 screens: Launch List and Rocket Detail. Since there are quite similar in code, so I will focus on explaining the implementation of Unit Testing in Launch List while you can read the code and learn more how I implement Unit Test to both screens.

Finally, lets create a folder called Networking and add a new file called APIServiceTests with the code below.

  1. We create 3 mock variables: mockSessionManager, mockDataRequest and mockBaseUrlString along with the sut. Same goes to setup and deinit in setUpWithError and tearDownWithError functions.
  2. Here we will do an asynchronous test and use a new structure called Given, When, Then to run the test.
  3. We apply the same logic to test the error response.

Asynchronous Test

In step 2 above, we wrote the code to run asynchronous test to the API call. An API call is an asynchronous function because we will call to the SpaceX public API and there will be a delay before the result is returned. So we cannot purely rely on the assertions to verify the test as it won’t be accurate until we have received the response from the API.

Here we use another entity in XCTest to unit test the API call, which is XCTestExpectation. There are a few entities that we use with XCTestExpectation:

  • expectation(description: String) – Initialise and create a new XCTestExpectation instance with description, the description will be shown in the logs when the test is failed.
  • exp.fulfill() – Marks the expectation as having been met.
  • wait(for: [exp], timeout: 2.0) – Waits for the test to fulfill a set of expectations within a specified time.

So in a laymen terms, in the code above, we are telling XCTest to wait for the expectation to be fulfilled within the timeout period, which is 2 seconds. Here are the possible outcomes of the test:

  • If exp.fulfill() is called within the timeout period, the test is successful.
  • If exp.fulfill() is called after the timeout period, the test is failed.
  • If API doesn’t return the result after the timeout period, the test is failed.

Given, When, Then

You will also notice we added some comments: given, when and then. These are the structure that we follow when we write our test to improve code readability and consistency.

  • Given – Here we set up any variables as needed. for example, we control and setup the mock variables and the expected result in the code above, so that we can compare the result with the expected result.
  • When – Here we will execute the code being tested. In this case, calling the API.
  • Then – Here we will assert the result we expect. In this case, we write the wait function to wait for the expectation to be fulfilled within the timeout period of 2 seconds, else we tell XCTest to fail the test.

Before we add the unit tests for ViewModels and VIewControllers, lets create 1 more file in Helpers folder, called XCTest+Extensions and add the following code into it. What it does is actually letting us to create several Launch models easily and quickly, we will use it in LaunchListViewModelTests.

Then, lets create a new folder called ViewModels and copy-paste the code below and create these 2 files: LaunchListViewModelTests and LaunchListTableViewCellViewModelTests.

In the LaunchListTableViewCellViewModelTests, we did things a bit differently, basically we are combining LaunchListTableViewCellViewModel and LaunchListTableViewCell into 1 test case. Is it proper to do it this way? Not really but it is up to you whether you to decide. The reason I decided to do it this way is because the code in LaunchListTableViewCell is very simple, only to display the value from LaunchListTableViewCellViewModel, so I will just combine them into 1 test case and use the cell to verify the result.

  1. Here we declare both sut and the cell variables.
  2. We initialise SUT with Launch model.
  3. We are simulating how LaunchListTableViewCell is initialised and dequeued in the tableView in LaunchListViewController.
  4. After cell has been asigned in step 3, we assigned sut to cell.viewModel, so that the cell displays what is in the sut.

With the helper functions in 3 and 4, then we can write the test functions to verify the result accordingly.

  1. Here we are creating mockVIewController to simulate LaunchListViewController.
  2. We pass the instance of MockAPIService into LaunchListViewModel using the technique of Dependency Injection, so that we can control and return the result we want to test the class.
  3. This is another way to initialise XCTExpectation and condition to fulfill it. In order for the expectation to be fulfilled, the predicate must return true. Finally, we also need to pass in the object to be evaluated in the predicate, in this case, mockViewController. By the way, since LaunchListViewModel and LaunchListViewController are 2 ways binding with RxSwift, so the driver in mockViewController is also an asynchronous function, therefore we need to use XCTExpectation to verify the result.
  4. Here we set up mockViewController, assign sut to its viewModel and run loadViewIfNeeded(), so that it simulates how viewController loads viewModel when it is being loaded.
  5. Here we create a helper function that relys on the helper function that we have created before to create multiple LaunchListTableViewCellViewModel instance and assign them to launchViewModels easily.

Lastly, let us create a folder named Views and add LaunchListViewControllerTests with the code below.

  1. In LaunchListViewControllerTests, we will try to simulate the ViewController’s and TableView’s lifecycle in when section to run our tests.
  2. Again, we create a simple helper function to help us to dequeue and return the cells.

To be able to see the test coverage, you can go to edit scheme, Test, Options tab, check Gather coverage for some target and add SpaceXLaunch. You can now run the test again and go to the Report Navigator, choose the latest test and select Coverage to see the test coverage for each file in the project.

You can download the project from GIthub.

Feel free to comment to give your suggestion or ask me any questions. Thank you.

Leave a Comment

Your email address will not be published. Required fields are marked *