Xcode Test Plans for iOS: Getting Started [FREE]

Today, writing automated tests is an integral part of the development process. It’s as important as programming itself and can be just as time-consuming. Proper tests enrich technical documentation and prevent future bugs as the app evolves. Fortunately, Xcode offers all the tools you need for full test coverage, and Xcode test plans make those tools easier to use.

Sometimes it’s tricky to test all your app’s functionality under multiple conditions. The more combinations you want to cover, the more test schemes or even separate test targets you need to create. This is where the new Xcode test plans come in handy.

In this tutorial, you’ll add test plans to a simple budget app. In the process, you’ll learn:

  • How to create an Xcode test plan.
  • Why you should create multiple test plans in an app.
  • How to test in multiple localizations.
  • How to use code diagnostics and memory management configurations in your test plans.

It’s time for some fun with Xcode test plans!

Getting Started

The project you’ll work on is a budget app written in SwiftUI.

Start by downloading the project materials using the Download Materials button at the top or bottom of this tutorial. Open the starter project and build and run.

As you can see, you can create multiple accounts by tapping the plus button in the top-right corner. You can manage balances by registering expenses and profits.

Testing the Tests

Now, go back to the project and take a look at the test classes, AccountsViewModelUnitTest and AccountsViewUITest :

AccountsViewModelUnitTest verifies that AccountsViewModel can add and remove accounts and manage balances correctly. AccountsViewUITest validates a few simple scenarios from a user perspective.

Run the tests by pressing Command-U . Open Test navigator to see the test results:

Note : If some of the UI tests fail, disable the Connect hardware keyboard option.

To do this, choose the I/O menu option in the simulator app, then go to Keyboard and uncheck Connect hardware keyboard . UI tests seem to have trouble accessing text fields in the simulator when a hardware keyboard is connected.

All tests are green, and you’re good to proceed!

Unit Tests and UI Tests

Note : If you don’t feel confident working with tests on iOS, or you need to refresh your knowledge, have a look at our iOS Unit Testing and UI Testing Tutorial . Then come back and join us in learning about Xcode test plans!

Unit tests check whether each unit of code, such as a class or a function, produces the expected result. Unit tests run in isolation, without dependencies on other modules or components. Because of this, they consume little time or resources. You should never hesitate to write more unit tests! :]

End-to-end tests validate your app from launch to finish. One of the tools you can use for end-to-end testing is a UI test , which replicates interaction with the app exactly the way a user would interact with it. UI tests are much slower and more expensive to run than unit tests.

You should try to avoid writing a UI test when you can cover the scenario with a few unit tests. Extensive UI tests are mostly worth using to cover core functionality .

As you see from the test report above, every time you run your tests, you run both UI and unit tests. This is convenient early in your development process. As your app grows, however, you’ll want to run different types of tests more or less frequently at different points of time.

For example, you might consider executing unit tests on each push to your feature branch and running end-to-end tests before merging your feature branch to your develop branch.

This is where Xcode test plans come in!

Creating Your First Test Plans

A test plan allows you to control which tests run at a specific time. In this section, you’ll create two test plans: one for unit tests and another for both unit and UI tests.

Creating a Unit Test Plan

Your first test plan will run unit tests only. Follow these steps to create and configure your test plan:

  1. Click the current scheme name, BudgetKeeper , and select Edit scheme… .
  2. In the Test section, click Convert to use Test Plans… .

  3. In the pop-up that appears, choose Create empty test plan and click Convert… .
  4. Name it UnitTests.xctestplan and click Save . Close the scheme editor by clicking Close in the scheme editor pop-up. See note below.
  5. You now have a UnitTests.xctestplan file in the root of your project. Select it and click Add Test Target , using the small + icon near the bottom.
  6. Choose BudgetKeeperTests and click Add .

Note : When you save your UnitTests.xctestplan in step 4 above, Xcode may suggest a default save position inside the BudgetKeeper.xcodeproj file — and then fail when trying to save it. To prevent this failure, make sure you save UnitTests.xctestplan in the root of your project folder and not inside BudgetKeeper.xcodeproj .

Now, press Command-U to run the tests and look at the Test navigator :

You’ll see that with your new test plan, only the unit tests are being executed. Great!

Now you need a plan to run all the tests.

Creating a Full Test Plan

Create another test plan by repeating the steps above. Name the plan FullTests.xctestplan . Select both BudgetKeeperTests and BudgetKeeperUITests as your test targets. If you do this correctly, you’ll see both plans in the current scheme:

Set FullTests.xctestplan as your default plan for now, as you’re going to take a closer look at UI tests in the next section.

Note : You can access the scheme editor more quickly by Option-clicking the scheme name in Xcode.

Testing Multiple Localizations

There are several localization files included in the project:

Turns out, your app already supports two languages — English and German. Großartig ! :]

Setting up Configurations

If your app depends on the user’s region or language, it makes sense to run UI tests for a few languages and regions. Your budget app, with its use of currency symbols, is a good example of this.

Set up your FullTests plan to execute UI tests in both English and German by creating two separate configurations:

  1. Select FullTests.xctestplan . In the Configurations tab, rename Configuration 1 to German by pressing Enter and replacing the existing text.
  2. In the configuration settings, set the Application Language to German . Set the Application Region to Germany :
  3. Still in the Configurations tab, click + to add a new configuration.
  4. Select the newly-created configuration file and press Enter to rename it to English .
  5. In the English configuration, set the Application Language to English . Set the Application Region to United States .

Running the Localization Tests

Go to AccountsViewUITest and select Run in All Configurations . Do this by right-clicking the Play button to the left of AccountsViewUITest :

Then, in the same menu, choose Jump to Report to review the test report:

Oh no, it failed! How can you fix it?

Start by looking at testUpdateBalance() . Expand the report for the German configuration of that test to find the cause of the failure:

Click the Eye icon near the Automatic Screenshot text:

Apparently, you’re using the wrong currency symbol for the specified region: Germany. The currency appears in dollars, but it should be euros!

Adapting the Code for Locale-Dependent Tests

Go to AccountView.swift to fix the issue. You’ll find the computed property balanceString there:

private var balanceString: String {
  let amount = AmountFormatter.string(from: abs(account.balance)) ?? ""
  let balance = "$" + amount
  return account.balance < 0 ? "-" + balance : balance

The dollar sign is hard-coded! Hard coding, as you know, is rarely the solution, and this is no exception. :]

Update your code:

private var balanceString: String {
  // 3
  let amount = AmountFormatter.string(from: abs(account.balance)) ?? ""
  let balance = currencySymbol + amount
  return account.balance < 0 ? "-" + balance : balance

// 1
private var currencySymbol: String {
  // 2
  return Locale.current.currencySymbol ?? ""

Here's what you're doing in this code:

  1. You declare a new computed property, currencySymbol .
  2. You get the currency symbol from the current user locale. That way, the currency symbol will be for Germany and $ for the United States.
  3. Finally, you use the newly-declared property in balanceString instead of a hard-coded value.

Run the UI tests again and open the test report:

Oh! testAddAccount() still fails!

Apparently, something went wrong with the translation of the navigation title. Look into Localizable.strings (German) and you'll see that there's a translation for the title New Account :

"BudgetKeeper" = "BudgetKeeper";
"New Account" = "Neues Konto";
"Update Balance" = "Kontostand updaten";
"OK" = "OK";
"Save" = "Speichern";
"Enter the name" = "Gib den Namen ein";

What else could be wrong then? Go to CreateAccountView.swift to investigate.

OK, you set the navigation title by calling navigationBarTitle(_:) . So far, so good.

But the key is wrong! It should be New Account , not New account . Account has an uppercase A , not a lowercase a . Here's how you fix this:

.navigationBarTitle("New Account")

Run the tests once again to verify the correct localization:

Alles gut and way better! :]

Analyzing an App Alongside Testing

Xcode offers several tools to analyze your app as it runs, but many of them can't be used together or can affect performance. You also have to remember that the tools exist, and to turn them on (and off again), and to pay attention to the results. Good news! You can set up analysis tools as part of your test plans! Go to one of the configurations of your FullTests plan. Take a look at the settings on the bottom:


The three sanitizers will check the following things in your app while it's running:

  • Address Sanitizer (ASan) detects memory usage errors in your code. Turning on ASan can slow down your tests a bit and increase memory usage, but it's worth it to ensure your code is free of memory violations.
  • Thread sanitizer (TSan) helps you detect data races , where multiple threads access and modify a memory area in an unsafe way. These are extremely difficult to find and debug, as they are quite unpredictable.
  • Undefined Behavior Sanitizer (UBSan) detects issues like dividing by zero or accessing an array outside of its bounds.

Note : You can't turn on Address sanitizer and Thread sanitizer in the same configuration. A proper solution, in this case, would be to have one sanitizer in the German configuration and another in the English configuration.

Runtime API Checking

Main Thread Checker is on by default, and you should keep it this way for your tests. It will warn you if you use frameworks like UIKit or AppKit on a background thread. You should only update the UI in the main thread.

Memory Management

Malloc Scribble , Malloc Guard Edges and Guard Malloc help you detect usages of deallocated memory and buffer overruns . The latter means that your program overruns the bounds of a buffer while writing data into it. This results in overwriting storage adjacent to the buffer memory locations.

Note : You can't use these tools with each other or with ASan and TSan in the same configuration.

When the Zombie Objects setting is on, objects in your program won't be deallocated when they reach a retain count of 0. Instead, they'll stay half alive and half dead, just like zombies. You'll receive a warning when your program tries to access such an object.

Where to Go From Here?

Download the final project using the Download Materials button at the top or bottom of this tutorial.

Well done! You've now learned how to organize your tests and how to test in multiple localizations. This means there are no more excuses for not writing those tests!

If you want to deepen your knowledge of testing iOS apps, check out our awesome iOS Test-Driven Development by Tutorials book. You'll learn about new approaches, different types of tests, how to mock external services for testing and much more.

Also, to learn more about memory management tools in Xcode, take a look at Apple's Malloc Debugging documentation.

Thank you for reading along! If you have any questions or comments, don't hesitate to leave them below.