Hey there! If you’re looking to dive into the world of testing iOS applications on Swift, you’ve come to the right place. My goal is to help you learn Swift testing fast in a fun and engaging way. We’ll kick things off by exploring automated app testing and discussing the different types of tests you can perform. And to wrap it all up, I’ll show you how to build a simple testing workflow for your Swift app and share some best practices you should definitely follow. Let’s get started!
Automated App Testing
As you can imagine, the concept of Automated App Testing refers to the implementation of individual tests or testing workflows that don’t require human intervention.
In this case, an automated test would typically be either a collection of routines and operations carried out by a separate application or a workflow that is part of the project itself.
There are four common types of automated tests, Unit Tests, UI Tests, Performance Tests, and Code Coverage Tests.
Typically, tests are designed to target units of operation. In the case of Unit testing, these units are code that is stateless and serves a single purpose. However, in the case of UI tests, these units are specific user workflows or interactions that the user can perform.
Let’s dive into these two more closely.
What are Unit Tests in Swift?
As stated before, unit tests in Swift are operations and validations that can be designed and performed on an isolated unit of code. Think of it like the following.
You have a class that contains the code that performs all mathematical operations for a calculator app. This class includes all the functions and procedures that this calculator needs to perform so it can work as a calculator would. So performing unit tests in this app would imply designing individual test cases for each operation in the class.
In terms of frameworks to perform Unit Tests, Apple provides its native framework called XCTest.
What are UI Tests in Swift?
If unit tests are focused on evaluating individual operations at the code level, UI tests are focused on evaluating individual operations at the user level.
UI tests emulate user interactions with the application with events like clicks and inputs that a host program can trigger programmatically. Essentially, it’s a script of interactions that you can design to simulate the different ways a user would interact with the app to validate the visual stability and performance of the application.
In Swift, UI testing is performed by identifying the UI elements that must be interacted with in each view and querying them, then synthesizing events that the XCUIApplication API can trigger.
Writing Swift Automated Tests
Alright, let’s get our hands on the code.
For the purpose of this article, we will be using a project that already has some code on it to save some time. You can find it in this repository.
If you want to know how to set up a new project with tests, open XCode and create an iOS app project and don’t forget to tick on the “include tests” checkmark. Not that XCode will do all the work for us, but it helps to set up all the groundwork.
Now that you have the project set up, explore it and run it. It’s not a complicated app by any means, so you should be able to see how it works pretty quickly.
Once you’re done, check out what’s in the SwiftTestingSampleTests folder.
As you can see, XCode already gives you an excellent framework to work with from the get-go. This code comes as is on every project that is set to have testing included.
You have a setup, a teardown, and a testExample operation. This last one is a template for your Unit tests.
Now, if you go to the SwiftTestingSampleUITest you will find two files with the groundwork for UI testing.
One file is there to serve as a proxy for setting up the launch of the app with the proper context and parameters, and the other is pretty much identical to the Unit Test one, following a structure of setup, teardown, and units of tests.
Let’s write a simple test to validate that the calculateResult() function works as intended.
Writing Unit Tests in Swift
Open the SwiftTestingSampleTests.swift file and add the following property.
// Reference of the view controller that will be subjected to tests
var sut: ViewController!
This property will serve as a reference to the view controller that you will test on this workflow.
Now, modify the setupWithError() and the tearDownWithError() with the following code.
override func setUpWithError() throws {
ย ย ย ย // Put setup code here. This method is called before the invocation of each test method in the class.
ย ย ย ย // Retrieve a reference of the main storyboard
ย ย ย ย let storyboard = UIStoryboard(name: "Main", bundle: nil)
ย ย ย ย // Initialize the view controller to be tested
ย ย ย ย sut = storyboard.instantiateInitialViewController() as? ViewController
ย ย ย ย // Load the view controller if needed
ย ย ย ย sut.loadViewIfNeeded()
}
override func tearDownWithError() throws {
ย ย ย ย // Put teardown code here. This method is called after the invocation of each test method in the class.
ย ย ย ย // Flush references
ย ย ย ย sut = nil
}
During setup, we initialize the view controller and load all its views.
During the teardown, we flush the references to clear all the resources and reset the test state.
This process is essential to ensure that all test cases are consistent and have the same context, avoiding issues with the app state or some global variable having different values affecting how the test responds.
Now create a new test case named testResultsAreCalculatedProperly.
func testResultsAreCalculatedProperly() throws {
}
It is important to note that all test cases must start with the word “test.” Otherwise, XCode won’t recognize them and won’t execute them.
Add the following code to the test.
func testResultsAreCalculatedProperly() throws {
// Wait for 1 second
sleep(1)
// Retrieve and validate if the element exists and is setup
let display = try XCTUnwrap(sut.display, "Display text field not properly setup")
// Set initial values for the test
display.text = "5+5"
sut.numberArray = [5, 5]
sut.operation = "+"
// Wait for 1 second
sleep(1)
// Trigger operation
sut.calculateResult()
// Validate expectation
XCTAssertEqual(display.text, "10", "Results are not being calculated properly!")
// Wait for 1 second
sleep(1)
}
As you can see, the test is pretty self-explanatory. First, you retrieve the elements and set values necessary to set up an initial state. Then you trigger the operation to be tested. And finally, you assert your expectation.
Notice how there are some calls to the sleep() method. The reason is to make the test more visible to us, but they are unnecessary and would hinder the performance of large workflows of tests.
There are many different kinds of assertions at your disposal.
- XCTAssertNill / XCTAssertNotNil
- XCTAssertTrue / XCTAssertFalse
- XCTAssertThrowsError / XCTAssertNoThrow
- XCTAssertGreaterThan / XCTAssertLessThan
- XCTAssertIdentical / XCTAssertNotIdentical
All these follow a similar format and allow you to evaluate assumptions about the output of your operations.
You can find more on the official Apple documentation here.
Writing UI Tests in Swift
Unlike the Unit Test setup, here you won’t need to make a setup or teardown setup, at least for this simple test.
Create the same testResultsAreCalculatedProperly() test case and add the following code.
func testResultsAreCalculatedProperly() throws {
// UI tests must launch the application that they test.
let app = XCUIApplication()
app.launch()
// Set initial values for the test
// Wait for 1 second
sleep(1)
// Trigger a button tap event
app.buttons["5"].tap()
// Wait for 1 second
sleep(1)
// Trigger a button tap event
app.buttons["+"].tap()
// Wait for 1 second
sleep(1)
// Trigger a button tap event
app.buttons["5"].tap()
// Wait for 1 second
sleep(1)
// Trigger a button tap event
app.buttons["="].tap()
// Wait for 1 second
sleep(1)
// Retrieve an element from the app current instance
let display = app.staticTexts.element(matching: .any, identifier: "display")
// Validate expectation
XCTAssertEqual(display.label, "10", "Results are not being calculated properly!")
// Wait for 1 second
sleep(1)
}
Here, setting the initial values for the test involves interacting with the application by emulating a user’s behavior. This is achieved throughout the events API that XCode offers you to issue tap events and also input on the elements directly.
As you can see, the app property gives us access to the available elements on the screen through collections. You can, for example, access the buttons on the screen through the buttons collection, the labels through the staticTexts collection, the text fields through the textFields collection, and so on.
The rest of the test is carried out in a very similar manner. We instantiate the necessary references, set up an initial state, and trigger the operation to test and evaluate our expectations.
You can run the test and see how they behave. The whole test code can be found on the repository in the testSamples branch.
Moving On
As you can see, you only need a few minutes and a great guide to learn swift testing fast and make progress. But testing is just one aspect of the process of development, albeit a very critical one.
If you want to continue expanding your knowledge of iOS, check out my post on how to master WebSockets in iOS. Not only will you learn how to work with WebSockets, but you will have a nifty chat app at the end. Check it out here.
This post was originally written for and published by Waldo.com
Leave a Reply