Waldo sessions now support scripting! – Learn more
App Development

A Guide to Using XCTestCase for Tests

Juan Reyes
Juan Reyes
A Guide to Using XCTestCase for Tests
January 31, 2023
8
min read

One of the most critical aspects of software development is testing. It helps to ensure that a software application works as expected and that any changes made to the code do not break existing functionality. One way to perform testing in iOS applications is by using the XCTest framework and the XCTestCase class.

What Is XCTestCase?

The XCTest framework is a testing framework provided by Apple for writing and running unit tests in iOS and macOS applications. It is built on top of the lower-level OCUnit framework created for Objective-C and provides a more modern and user-friendly interface for writing and executing tests.

The XCTestCase class is a crucial component of the XCTest framework. It provides a template for writing test methods and includes a set of utility methods for setting up and tearing down the test environment. Additionally, it provides tools for asserting the expected behavior of the code under test.

Here's how you can create a test case using the XCTestCase class from the beginning:

  1. Create a new iOS project in Xcode.
  2. Select the File > New > Target menu option.
  3. Select iOS > Test > Unit Test Bundle in the target template dialog.
  4. Give your test case class a name and choose the language (Swift or Objective-C) you want to use.
  5. Click the Finish button to create the test case class.

Once you have created a test case class, you can add your test methods and make a test workflow.

XCTestCase Basics

A test method is a piece of code that exercises a specific aspect of the code under test and verifies that it is working as expected. To create a test method, you must define a new method in your test case class and prefix its name with test. Following the nomenclature is important because, otherwise, XCode won't identify it as a test case and will ignore it.

Here's a simple test method written in Swift:


func testExample() throws {
    // Given
    let input = 5
    let expectedOutput = 25
    // When
    let actualOutput = input * input
    // Then
    XCTAssertEqual(actualOutput, expectedOutput)
}

This test method tests a hypothetical function that squares a number. It first sets up the input and expected output for the test. It then calls the code under test (in this case, the hypothetical function that squares a number) and stores the result in a variable called actualOutput. Finally, it uses the XCTAssertEqual method to verify that the output matches the expected outcome.

The XCTestCase class also provides several utility methods that you can use to set up and tear down the test environment. These include the setUp and tearDown methods, which are called before and after each test method. You can use these methods to perform any necessary setup or cleanup tasks, such as creating mock objects or resetting the state of the code under test.

XCTestCase Structure and Assertions

Here is the basic structure of a test case class with setUp and tearDown methods written in Swift:


final class SwiftUIXCTestCaseSampleTests: XCTestCase {
    override func setUpWithError() throws {
       // Perform any necessary setup tasks here. This method is called before the invocation of each test method in the class. 
    }
    
    override func tearDownWithError() throws {
        // Perform any necessary cleanup tasks here. This method is called after the invocation of each test method in the class.
    }
    
    func testExample() throws {
        // Test code goes here
        // This is an example of a functional test case.
        // Use XCTAssert and related functions to verify your tests produce the correct results.
        // Any test you write for XCTest can be annotated as throws and async.
        // Mark your test throws to produce an unexpected failure when your test encounters an uncaught error.
        // Mark your test async to allow awaiting for asynchronous code to complete. Check the results with assertions afterwards.
    }
}

The XCTestCase class also includes some assertion methods that you can use to verify that the code under test is behaving as expected. These include methods like XCTAssertTrue, XCTAssertFalse, XCTAssertEqual, XCTAssertNotEqual, XCTAssertNil, and XCTAssertNotNil.

Here are a few examples of these assertion methods in action:

In addition to these basic assertion methods, the XCTestCase class also provides many more advanced assertion methods that allow you to perform more complex verifications. One example includes using the XCTAssertThrowsError method to verify that a block of code throws an error. Another example would be to use the XCTAssertEqualWithAccuracy method to compare floating-point values with a certain level of precision.

Finally, the XCTestCase class includes several performance-related methods that allow you to measure the performance of your code. These methods include the measure block, which enables you to measure the time it takes to execute a block of code, and XCTestCase.measureMetrics, which allows you to measure various performance metrics, such as CPU usage and memory usage.


func testPerformanceExample() throws {
        // This is an example of a performance test case.
        measure {
            // Put the code you want to measure the time of here.
        }
    }

Building a Simple Test Workflow With XCTestCase

OK, now that we understand the basics of the XCTestCase testing tool, let's build a more complex example that would look more like what you could find in most project testing workflows.

First, let's create a view model class containing the logic of the view, which in this case, will be a simple addition view.


import Foundation

final class ViewModel: ObservableObject {
    @Published var input1: String = "0"
    @Published var input2: String = "0"
    @Published var output: String = "0"
    
    func calculate() {
        output = "\(Int(input1)! + Int(input2)!)"
    }
}

This view model contains two inputs and an output. The calculation takes place in the calculate function. Since this class is an observableObject, and all the properties are marked as @published, the view will update automatically when the value of these properties change.

Next, modify the main content view to display a simple calculating view:


struct ContentView: View {
    @StateObject private var viewModel = ViewModel()
    
    var body: some View {
        VStack {
            HStack {
                TextField("",  text: $viewModel.input1)
                    .padding()
                    .border(.gray)
                
                Text("+")
                
                TextField("", text: $viewModel.input2)
                    .padding()
                    .border(.gray)
                
                Text("=")
                
                TextField("", text: $viewModel.output)
                    .padding()
                    .border(.gray)
            }.padding()
            
            Button {
                viewModel.calculate()
            } label: {
                Text("Calculate")
            }.buttonStyle(.borderedProminent)
        }
        .padding()
    }
}

Heads up: Ensure the class files are included in the test target on the target membership section. Otherwise, your test class won't be able to see the contentView class and access its code.

Building the Test Cases

Now, go to the testing class and add the following code:


func testCalculateResult() throws {
        // Given
        let input1 = "10"
        let input2 = "20"
        let expectedResult = "30"
        
        // When
        let subject = ViewModel()
        subject.input1 = input1
        subject.input2 = input2
        subject.calculate()
        
        // Then
        XCTAssertEqual(subject.output, expectedResult)
    }
    
    func testCalculateResultWithInvalidInput() throws {
        // Given
        let input1 = "aaa"
        let input2 = "20"
        
        // When
        let subject = ViewModel()
        subject.input1 = input1
        subject.input2 = input2
        
        // Then
        XCTAssertThrowsError(subject.calculate())
    }
    
    func testPerformanceExample() throws {
        // This is an example of a performance test case.
        measure {
            // Put the code you want to measure the time of here.
            let _ = ViewModel().calculate()
        }
    }

In this example, the test case class tests the functionality of our ViewModel class in the application. The test case class includes three test methods: testCalculateResult(), testCalculateResultWithInvalidInput(), and testPerformanceExample().

The testCalculateResult() method tests the calculate() function of the ViewModel class with valid input. It sets up the test by defining the input and expected result, calls the calculate() function, and then uses the XCTAssertEqual method to verify that the actual result matches the expected result.

The testCalculateResultWithInvalidInput() method tests the calculate() function with invalid input. Here we expect the function to crash because the input provided is not a number. This test is similar to the first one but uses different inputs.

Finally, the testPerformanceExample() method measures the performance of the calculate() function using the XCTestCase measure block. This allows us to verify that the function performs efficiently and scales well with increasing input size.

Overall, this example demonstrates how we can use the XCTestCase class to write and run a variety of tests for an iOS application. By using a combination of setup and tear-down methods, assertion methods, and performance-related methods, you can thoroughly test the functionality and performance of your iOS application. 

You can find the complete code in this repository.

Moving On

In conclusion, the XCTestCase class is a powerful tool for testing iOS applications. It provides a set of utility methods for setting up and tearing down the test environment, as well as a variety of assertion methods for verifying the behavior of the code under test.

Using the XCTestCase class and the XCTest framework ensures that your iOS application is working correctly and that any changes you make to the code do not break existing functionality.

Developing test workflows can be a very time-consuming task for a team, let alone a single developer. Yet there's no better way to make sure your work stands up to scrutiny and use. If this is important for you and you need more time or resources to ensure this, check out Waldo's extensive toolbox for UI testing to ensure your code is secure and resilient. Even for nondevelopers, it is incredibly accessible and doesn't require any code.

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.