How to implement UI Tests with SwiftUI — A few examples

Joana Lima
Apple Developer Academy | UFPE
6 min readOct 26, 2023

UI testing in a SwiftUI app is critically important for many reasons. It ensures the quality and reliability of your app by identifying and preventing defects in the user interface, thereby enhancing the overall user experience. Through functional validation, it verifies that all features work as intended and helps catch regressions as your app evolves.

In this article, I will present a few examples of how to implement UI testing by showing how I did it in the app I created with my colleagues at Apple Developer Academy | UFPE. The app is called RecifeAR and is available on the app store for iPad so be sure to check it out. 😄

Note: I will assume you already know how to create a test file in Xcode as this article won't cover that.

So, this is the View I will be testing, on our app we have a few buttons, that that take you to another View, and localization, which means the app language changes based on the device's language. These are the main things we will test.

Suggested Dynamics View.

To run our tests, we want to start with a setUpWithError() function, which will run before all the other functions and ensure the app is being initialized the way you need it to be. The suggested dynamics view is not the first view on our app so you have to make the simulator go to the right screen, otherwise it will run the tests on the first view initialized when the app opens. You need it to click on the Suggested Dynamics Card (shown below) to go to the Suggested Dynamics View (shown above).

StartView of RecifeAR with Suggested Dynamics Card indicated by the red circle.

The code will be like this:

override func setUpWithError() throws { //Does this each time it starts the test
continueAfterFailure = false
app = XCUIApplication() // Initializes the XCTest app
app.launch() // Launches the app

//Makes it go to the right View
let sugDynamicsSgCard = app.buttons["SugDynamicsStTitle"]
XCTAssertTrue(sugDynamicsSgCard.exists)
sugDynamicsSgCard.tap() //The action he will execute, just as if you tapped the screen

}

But, for it to work, you have to add a modifier that serves to identify the component you're referring in your test file. It has to be put on the view you're working, modifying that specific component by putting .accessibilityIdentifier("Name_here").

The modifier will be added on the StartView under the Suggested Dynamics card, like this:

NavigationLink(destination: SuggestedDynamicsView()){
SmallCard(smallCardImage: "dinamicas-image",
smallCardText: Text(LocalizedStringKey("suggestedDynamics")),
smallCardDescription: Text(LocalizedStringKey("suggestedActv")))
}
.accessibilityIdentifier("SugDynamicsStTitle")

So now the app knows the name of the card (that works as a button) is "SugDynamicsStTitle" and will be able to find it.

It is also important to add a tearDownWithError() function, to ensure the app is closing correctly.

override func tearDownWithError() throws { //Does this each time it ends the test
app = nil //Makes sure that the test wont have residual values, it will be torn down each time the funcion has finished
}

Now the testing will really begin. First, we'll check if the View title is being presented. If one title on the screen is working, all of them are, so you can choose arbitrarily which text you want to use.

func testSuggestedDynamicsTitleDisplay() { //Checks if the title is being presented

// Use the XCUIElementQuery to locate the title Text
let suggestedDynamicsTitleText = app.staticTexts["withAppTitle"]

// Check if the title text is visible
XCTAssertTrue(suggestedDynamicsTitleText.exists)
}

For it to work, we also have to add the identifier to name the component in the Suggested Dynamics View Text were the title is created. This will serve in the future to test if the localization is working, that way you won't need add another identifier.

Text(LocalizedStringKey("withApp"))
.font(.custom("ObviouslyVar-SmBd", size: 24))
.foregroundColor(Color("Azul-950"))
.padding(.top, 18)
.padding(.leading, 32)
.accessibilityIdentifier("withAppTitle") //Modifier that identifies the title for the test to find it

To check if the localization is working (meaning: the language is switching accordingly), we will have to create a variable that gets the language used in the device and compare to the one that is being presented in the app. We currently are working with portuguese and english, so we only have to take this two cases into consideration. If you have more languages you can add more if statements to cover all of them.

func testSuggestedDynamicsLanguageBasedBehavior() { //Tests if if the language is switching

let preferredLanguages = Locale.preferredLanguages
let suggestedDynamicsTitleText = app.staticTexts["withAppTitle"]

if let currentLanguage = preferredLanguages.first {
if currentLanguage.hasPrefix("en") {
// Perform English-specific tests
XCTAssertEqual(suggestedDynamicsTitleText.label, "With RecifeAR")

} else if currentLanguage.hasPrefix("pt") {
// Perform Portuguese-specific tests
XCTAssertEqual(suggestedDynamicsTitleText.label, "Com o RecifeAR")
}
} else {
XCTFail("Unable to determine current language")
}
}

The function XCTAssertEqual works by comparing two informations and checking if they are equal, so you have to manually write the output you're expecting for it to be compared with the input you're receiving from the suggestedDynamicsTitleText.label.

The last function we'll implement is the one aimed to test if the button's navigation is working. We want to test if the info button is correctly taking you to the About Us View.

func testNavigationToAboutUsView() {
//Executes the action of tapping the button
let aboutUsButton = app.buttons["AboutUsButton"]
XCTAssertTrue(aboutUsButton.exists)
aboutUsButton.tap()

// Checks if you've navigated to the AboutUsView
let aboutUsViewTitle = app.staticTexts["AboutUsViewTitle"]
XCTAssertTrue(aboutUsViewTitle.waitForExistence(timeout: 5))
}

As shown before, you also have to add the identifier to the button.

NavigationLink(destination: AboutUsView(), isActive: $showSupportView){
AboutUsButton(title: Text(LocalizedStringKey("")), icon: Image(systemName:"info.circle") , action: {
self.showSupportView = true
})
}
.padding(.trailing, 32)
.accessibilityIdentifier("AboutUsButton")

Joining all the functions we created, the complete test file will look like this:

import XCTest
import SwiftUI
@testable import recifear

final class SuggestedDynamicsViewUITests: XCTestCase {

private var app: XCUIApplication!

override func setUpWithError() throws { //Does this each time it starts the test
continueAfterFailure = false
app = XCUIApplication() // Initializes the XCTest app
app.launch() // Launches the app

//Makes it go to the right View
let sugDynamicsSgCard = app.buttons["SugDynamicsStTitle"]
XCTAssertTrue(sugDynamicsSgCard.exists)
sugDynamicsSgCard.tap() //The action he will execute, just as if you tapped the screen
}

override func tearDownWithError() throws { //Does this each time it ends the test
app = nil //Makes sure that the test wont have residual values, it will be torn down each time the funcion has finished
}

func testSuggestedDynamicsTitleDisplay() { //Checks if the title is being presented
// Use the XCUIElementQuery to locate the title Text
let suggestedDynamicsTitleText = app.staticTexts["withAppTitle"]

// Check if the title text is visible
XCTAssertTrue(suggestedDynamicsTitleText.exists)
}

func testSuggestedDynamicsLanguageBasedBehavior() { //Tests if if the language is switching
let preferredLanguages = Locale.preferredLanguages
let suggestedDynamicsTitleText = app.staticTexts["withAppTitle"]

if let currentLanguage = preferredLanguages.first {
if currentLanguage.hasPrefix("en") {
// Perform English-specific tests
XCTAssertEqual(suggestedDynamicsTitleText.label, "With RecifeAR")

} else if currentLanguage.hasPrefix("pt") {
// Perform Portuguese-specific tests
XCTAssertEqual(suggestedDynamicsTitleText.label, "Com o RecifeAR")
}
} else {
XCTFail("Unable to determine current language")
}
}

func testNavigationToAboutUsView() {
//Executes the action of tapping the button
let aboutUsButton = app.buttons["AboutUsButton"]
XCTAssertTrue(aboutUsButton.exists)
aboutUsButton.tap()

// Checks if you've navigated to the AboutUsView
let aboutUsViewTitle = app.staticTexts["AboutUsViewTitle"]
XCTAssertTrue(aboutUsViewTitle.waitForExistence(timeout: 5))
}

}

Mistakes I made so (I hope) you won't:

  • Make sure that you're on the right screen, the simulator runs the tests as if its a person going through your app, so if you don't ensure the tests are being run on the right View, they will fail. A nice way to do that is by implementing this action right in the setUpWithError() so you don't have no run that inside every single function.
  • Make sure to add the right name for the .accessibilityIdentifier modifier, sometimes the test will fail and it's just because you copied and pasted from another place and forgot to change the name inside the quotation marks.
  • Make sure to add the @testable import recifear. If you don't, the test doesn't have access to your app files and won't be able to run properly.

I hope this article helped you in some way. Bye 😘

Check us out!!!

RecifeAR's Instagram or Download on the App Store

--

--