Waldo sessions now support scripting! – Learn more
App Development

An Introductory Guide to Using XCTest

Ifeanyi Benedict Iheagwara
Ifeanyi Benedict Iheagwara
An Introductory Guide to Using XCTest
November 8, 2022
7
min read

The XCTest framework is one of the few test automation frameworks tightly coupled with its development tool and environment. As a result, XCTest is the de facto framework for writing UI, performances, and basic tests for your native iOS application. The advantage is that it does not require any additional dependencies; hence, it provides a seamless workflow to its users. 

In this post, we'll provide an introductory guide to XCTest. But first, we'll cover what it is, how you can use it, and some best practices to keep in mind.

XCTest is a unit-testing framework in Xcode used to test any code written in Swift

What Is XCTest?

XCTest is a unit-testing framework in Xcode used to test any code written in Swift. The main goal of this framework is to provide a simple and easy-to-use interface to test Swift code. XCTest, in this instance, acts as the base class and is used to create, manage, and execute functional, unit, and performance tests.

Apple released XCTest alongside iOS 10 at the WWDC 2016 conference. Prior to XCTest, the only way to automate testing on iOS was through the UI Automation framework using instruments. However, UI Automation had some notable shortcomings, such as the need for a Mac and basic JavaScript coding knowledge. XCTest eliminates these issues by removing the need for a Mac and letting tests run directly on an iPhone or iPad.

How to Use XCTest

XCTest relies on the XCTest framework to write tests. It also simplifies the whole testing process. All you need to do to get started is create a new project in Xcode and choose the "iOS Unit Testing Bundle" template. 

Let's see how you can use XCTest:

  1. Open Xcode.
  2. Create a new project, and check the "Include unit test" box.
  3. Select "iOS Unit Testing Bundle."
  4. Enter the product name.
  5. Click Finish.

To run unit tests in Xcode, you can use the menu bar or the keyboard shortcut (⌘U). It's important to ensure everything is in order. You should see green check marks in the Test Navigator when the tests are finished, which means the tests passed.

An essential aspect of knowing how to use XCTest is being able to write good tests. The testing process checks that the written code meets the specified requirements for the application. Let's use an example.

"The login page should have the company name." We'll call the company "Rentol."

The newly created test file should contain this structure:


import XCTest
class ExampleTestCase: XCTestCase {
...
override func setUp() {
...
// Put setup code here. This method is called before the invocation of each test method in the class.
super.setUp()
}
override func tearDown() {
...// Put teardown code here. This method is called after the invocation of each test method in the class.
super.tearDown()
}
func testExample() {
...
// This is an example of a functional test case.
// Use XCTAssert and related functions to verify your tests produce the correct results
}
func testPerformanceExample() {
...
// This is an example of a performance test case.
self.measure {
...
// Put the code you want to measure the time of here.
}
}
}

The first thing to note in the structure is the import XCTEST, which is responsible for the test framework functioning in the file, then the class Rentol: XCTestCase within the class. There are three major groups:

  1. setUp() method: Any code required before the test begins is written within this method.
  2. tearDown() method: Any code required after the test ends is written within this method.
  3. testExample() method: This holds the main testing code. For Xcode to recognize the test function, the testing code must have the prefix test.

Now that you know the structure, let's write a quick test for the earlier example. 


func testCompanyNameRentol() {
    let storyboard = UIStoryboard(name: "Main", bundle: nil)
    let login = storyboard.instantiateInitialViewController() as! LoginViewController
    let _ = login.view
    XCTAssertEqual("Rentol", login.titleLabel!.text!)
}

Now let's run our test. First, select the Rentol.swift file, then click on Run. Once the test is complete, a summary will show in the output section.

XCTest Features

Here are some features of XCTest.

Debugging

Like every other testing tool, XCTest has various methods for debugging. One instance is to see the UI elements of an application.


print(app.debugDescription)

This code will provide a description of the app, including its UI elements. This is very helpful if there is a problem.

Assertions

The syntax for an assertion is:


XCTAssert(condition, message)

It is used to check if a condition is true. It fails if the given condition is invalid. For example, you can use this syntax to check whether two values are identical.


    XCTAssertEqual(expression1, expression2, "optional description")

If the values are equal, then the test will continue. Still, if it's not, it will fail, and the test will stop.


    XCTAssertEqual(1,3," The numbers are different")

The test will fail in this case because the values are not equal. It will print out the description "The numbers are different."

Matchers

To find elements in the UI of an application, you can use a matcher. You can also use it to check if an element exists. Let's say we want to find the "Sign Up" button or check if it exists at all.

To find the button, you can use Let button = app.buttons["Sign Up"]. And to check if the element exists, use XCTAssertTrue(app.buttons["Sign Up"].exists, "Sign Up button does not exist")

Just like the earlier assertion example, if the button does not exist, the description “Sign Up button does not exist” will be the output.

When running UI tests, the possibility of an element not appearing on time is very common

Existence

When running UI tests, the possibility of an element not appearing on time is very common. To ensure that your test does not run into an unexpected error, the XCUIElement.waitForExistence(timeout:) method can be used.

Let’s say you want to sign up for an app, but for some reason the "Sign Up" button fails to show up. Ideally, the test would fail, but with this function, it will wait for a specified time before it times out. Below is the code for the example explained:


    let button = app.buttons["Sign Up"]
    let buttonExists = button.waitForExistence(timeout: 3)
    XCTAssertTrue(buttonExists)
    button.tap()

If the button labeled "Sign Up" does not appear within three seconds, an error will be thrown, making the test fail.

Disabled Elements

Some elements might be disabled when the test starts. There are several reasons that this can happen. The most common is the user not logging in, or the app being in a specific state. The XCUIElement.isEnabled property is used to check if an element is enabled. Let’s see how this can be used:


    let button = app.buttons["Sign Up"]
    if button.isEnabled {
    button.tap()
    } else {
    }

Element Exists

Some test elements may not exist when the test starts. This can happen because the element is only shown after a specific action is taken. Therefore, before interacting with an element, it's necessary to check if the element exists. The XCUIElement.exists property is used to check if the element exists. Here's how to use this:


    let element = app.buttons["Sign Up"]
    if element.exists {
    element.tap()
    } else {
    
    }

Best Practices for Unit Testing with XCTest

Now let's go over some best practices to keep in mind when using XCTest.

Format

It is best to divide the test into given, when, and then sections.

  • Given: This is where you enter any required values.

  • When: In this section, you will run the code that is being tested.

  • Then: In this section, you'll check the expected result with a message that contains if the test fails.

For example:


    func testScoreIsInputedWhenNewValueIsHigherThanMain() {
        // given
        let newvalue= sut.mainValue + 5
      
        // when
        sut.check(newValue: newValue)
      
        // then
        XCTAssertEqual(sut.scoreRound, 80, "The new value is different")
      }

Throwing Methods

You can define a throwing test method the same way the application code can. This allows you to make a test fail when a method within the test throws an error.


    func testDecoding() throws {
        /// When the Data initializer is throwing an error, the test will fail.
        let jsonData = try Data(contentsOf: URL(string: "user.json")!)
    
        /// The `XCTAssertNoThrow` can be used to get extra context about the throw
        XCTAssertNoThrow(try JSONDecoder().decode(User.self, from: jsonData))
    }

When the result of the throwing method is not required in any subsequent test execution, you can use the XCTAssertNoThrow method.

Testing Performance

A performance test is easy to create: Enter the code you want to measure into the measure closure (). You can also specify multiple metrics to be measured.

Some test metrics are as follows:

  • XCTStorageMetric: This metric indicates how much data the tested code writes to storage.
  • XCTClockMetric: This calculates elapsed time.
  • XCTCPUMetric: This keeps track of CPU activity, including CPU instruction numbers, time, and cycles.
  • XCTMemoryMetric: This tracks the physical memory amount used.

Conclusion

XCTest is a powerful tool for testing your iOS apps. However, you need to add many basic tests to thoroughly cover the logic and make the UI testing more suitable for iOS apps. That's why Waldo is necessary and helpful. 

Waldo lets you skip through all that hassle by being easy to use and intuitive, making it the perfect tool for any developer, regardless of skill level. In addition, with Waldo, you can automate tests and ensure that your users have the best experience possible.

Automated E2E tests for your mobile app

Waldo provides the best-in-class runtime for all your mobile testing needs.
Get true E2E testing in minutes, not months.

Reproduce, capture, and share bugs fast!

Waldo Sessions helps mobile teams reproduce bugs, while compiling detailed bug reports in real time.