Maximize iOS Testing Coverage: Unit Test vs. UI Test

November 5, 2021
|
Kendall Gelner
iOS Developer using Xcode on laptop
Photo by
Zan

In this article, we'll talk about the differences between an iOS Unit Test and UI Test. We’ll also cover when you might want to choose one type of test over the other.

We'll also give some advice on how to move a iOS Unit Test into a UI Test. Also, we’ll look at when each is appropriate to use. Finally, some helpful tips for Unit Test and UI Test runs.

Xcode iOS UI Test and Unit Test dialogue

Two Types of iOS Tests

For those new we'll define what are the fundamentals of these two different kinds of tests. In both an iOS Unit Test and UI Test the test class inherits from XCTestCase. Then, the tests run in a simulator instance. So, what is the difference between the two kinds of tests?

iOS UI Test

A UI Test focused on manipulating UI elements. This is so you can test that UI interactions cause the correct behavior. UI Tests can also inspect values of UI elements to see if an app runs as expected. 

You can manually create UI Tests. Also, you can create them by recording a series of .UI actions. This sequence is repeatable via a test. You can also add further checks within the test to verify application state.

However, UI Tests can only manipulate and inspect UI elements. The test classes themselves are running in a separate process from your application. They have no access to the internal app state. They can only inspect what appears in the UI (via accessibility features of iOS).

UI Tests also have an important aspect. Between tests, the application launches separately.  Also, you are able to suspend and resume an application in a UI Test. This is because the test has access to an app instance to manipulate.

iOS Unit Test

A Unit Test is a test that runs within the same process as your app. This means it is free to call internal methods of the app or SDK you are testing (via an @testable import statement). Thus, you can inspect state while running, or activate specific methods for testing.

However, an iOS Unit Test cannot access anything related to the UI on screen. A Unit Test also cannot access the running XCUIApplication instance in order to do things like suspend or re-launch the application.

Also, each testing run spawns one testing application instance used for all Unit Tests.  Importantly, Unit Tests run sequentially when housed in separate files. Unit Tests may run in parallel when housed in a single file.

This diagram summarizes the difference between the two kinds of tests:

Diagram of XCTest and XCUITest interaction with SDK instance

How to Choose: Unit Test or UI Test

The choice to make aUI Test or Unit Test depends on what you are trying to test. If you want to verify screen content or interact with UI you should use a UI Test. If you want to inspect application or SDK state or call internal methods, you will generally start with a Unit Test.

However you may come to a point where you run into the limitation of either type of test.

In the middle of a UI Test you might want to verify some internal application state.

For an iOS Unit Test, other running tests can alter internal state. This can cause tests to fail that might not otherwise when run by itself.

In these cases, try to start up the application in a clean state where no other tests have run, even if other tests have run previously.

Sometimes that means you want to be able to re-launch the application in a certain way while still manipulating state inside.  So how can we work with the differences between the two test types?

Combine iOS Unit Test & UI Test Features

Given we may want to use features of both UI and Unit testing in some tests, what can we do?

A flexible approach involves combining both kinds of tests. Use a UI Test that accesses screen elements that trigger internal methods in your application. Then, populates text fields with results or state of your application/SDK.

As these UI elements exist purely for testing, you can have a hidden testing screen within an app that a UI Test can trigger. Alternatively, you can create a separate testing application for something like an internal SDK that holds UI elements just for testing.

The general approach to building a test that takes this approach is:

  1. Build a UI control (generally a button) for the testing screen. When tapped, run a small block of code to do the test you desire. Based on the result of running that test code, alter a text field or label on the UI with results.
  2. Set up an expectation for the test at completion (either complete or failed).
  3. Introduce a small delay to make sure the application is fully initialized.
  4. Find the UI element (generally a button) on your test screen that corresponds to starting your desired test code.
  5. Trigger the UI element with a .tap() call.
  6. After a delay long enough for the test code to complete, examine the UI control that will hold results of your test run (such as a label). Example results from the test.
  7. Get the value of the UIElement and test for expected results, using XCTAssert to check conditions for failure.
  8. Call expectation.fulfill() to indicate the test is over.

A Real-World Test

Let's see those steps in action within a real test:

The test relies on getting access to specific UI elements. A button to activate a purchase and a label where test results are stored.

The button itself is obtained by using the text from the button. For the label, we have to add a bit of additional code in the main application view controller that displays the purchase button and results label:

The XCUITest cases use Accessibility to get at ID values to recognize the label. An accessibility value is set to the results of the test so set it (and the label text) to an initial value that indicates the test has not yet completed.

The code for the button press is kept simple to do just one task. It could be more complex:

Clean Slate Testing

You may need to clear out application state to run a particular test. There are a few options:

For an iOS Unit Test, you can implement a method accessible via the @testable import that class out pre-existing state that may be set from other tests within the application.

For UI Tests, you can create a command line parameter that you can pass to a test on launch. This would be used clear out state from previous launches. It would also access a method similar to the one described for cleaning application state.

At times however, you may need to be sure a iOS Unit Test is run after a clean launch within other tests run.

In that case, you can move a Unit Test into a UI Test class. Putting the code you wish to test into the application, triggering it via a UI element, and putting the results into a label for the test case to verify as described previously.

For a fully clean start you can pass in a launch argument from your test case:

Note that app.launch() re-launches the application after the initial test being activated runs it.

The code in the application to look for arguments being passed and act on them looks like:

Final Thoughts

Testing is always a balance between deciding what things are important to test for, and what is possible with your tools. Techniques like these should expand what is possible to test. Plus, gain greater flexibility in testing your app to the fullest extent possible.

Sign up to our newsletter

Get the latest articles delivered straight to your inbox.
Thank you! Your submission has been received!
Oops! Something went wrong while submitting the form.
Nami logo white

Focus on your app experience

We'll handle the subscriptions.

Portrait photo of blog author
Kendall Gelner

Kendall Gelner is an iOS Architect at Nami ML. He is well regarded in the iOS development community for his technical knowledge and platform experience going back to the App Store launch. The last SDK Kendall was responsible for shipped inside of some of the most widely installed apps, reaching more than 200 million devices.

Similar articles

Read similar articles to this one

Quotes mark

Testimonials

Some client stories

"We spent hours researching the best ways to implement subscriptions and after many failed attempts we found Nami. We were able to go live with subscriptions in our Apple and Android apps in a matter of days."
Client portrait
Brian Pedone
Founder
Quiet Punch
Quiet Punch
"Nami helped us achieve a cross-platform solution for managing and sellingsubscriptions on Apple and Google. The Nami platform was flexible enough to handleour business requirements for in-app purchasing, allowing us to focus on our client'score domain and domain logic.”
Client Name
Client role
Company name
"Nami helped us achieve a cross-platform solution for managing and selling subscriptions on Apple and Google. The Nami platform was flexible enough to handle our business requirements for in-app purchasing, allowing us to focus on our client's core domain and domain logic."
Melody Morgan
Director, Engineering
Diamond
Diamond
"We spent hours researching the best ways to implement subscriptions and after many failed attempts we found Nami. We were able to go live with subscriptions in our Apple and Android apps in a matter of days."
Brian Pedone
Founder
Quiet Punch
Quiet Punch
"It took a couple of hours to incorporate their easy to use SDK. Nami provides a monetization machine learning solution, a paywall displaying what a user can purchase, and a whole suite of other useful features. As a result, it saved me development cycles so I could focus on other important things."
Mark Lapasa
Android Developer
Toronto App Factory
Toronto App Factory
"After spending a few days trying to implement subscriptions, I found Nami ML. I was able to complete in-app subscriptions within less than 3 hours."
Tanin Rojanapiansatith
iOS Developer
Transcrybr
Transcrybr

The best subscription experience starts with Nami

Get connected with one of our product experts to get started with your journey with Nami today.