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.

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:

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.