This article relies heavily on this Ray Wenderlich tutorial by David Piper, which is very helpful in learning to write tests in Xcode. If any of my writing conflicts with Piper’s, it’s probably a good idea to defer to Piper.¹
For a quick introduction to XCTests
and what they can do for your development cycle, you can check out another article I wrote here. This one’s going to focus on getting functional unit tests up and running in your Xcode projects. For some hints on best practices in writing unit tests, feel free to check out my next post.
Setting Up Your Project for Tests
…While starting a brand-new Xcode project
If you’re setting up a brand new Xcode project and you know you’re going to want to use tests:
After opening Xcode, selecting “Create a new Xcode project,” and selecting the template for your project (you probably selected “App”), simply be sure to check the box next to “Include Tests” when configuring your projects options (Figure 1).
When you’ve completed the project setup, you’ll have these “Test” folders in your Project Navigator hierarchy (Figure 2).
…With a preexisting project
If you’ve already got a project going and you now need to set up tests (you don’t see any “Test” folders in your Project Navigator hierarchy like in Figure 2):
Select your project in the top of the Project Navigator (it probably has the blue App Icon next to the name you gave your project), and then from the toolbar, select “Editor >> Add Target” and scroll down to the “Unit Testing Bundle” option (Figure 3). Select that, then hit “Next” and finally “Finish” on the screen after that.
This will set up your project for unit tests and you’ll see the folder and files as shown in Figure 2.
If you want to add UI Tests too, go ahead and follow the same process you did to add the Unit Testing Bundle, just this time, selecting “UI Testing Bundle” (seen on the left of the yellow rectangle in Figure 3).
If you’re using this process to add unit tests to your project (rather than adding them at project setup), just note that you’ll probably need to manually allow the Tests.swift files to have access to the project files containing relevant code as you continue building your project. For example, in the StealthyCalc app (Figure 4), to allow one of your tests access to the NumberCruncher
struct, you’d have to go to the file that the struct is declared in — NumberCruncher.swift (4.1) — and in the File Inspector (4.2), under “Target Membership,” select the boxes next to the Test bundle(s) where you’ll want to use the NumberCruncher
struct (4.3).
Alternatively, you can just include the line
@testable import StealthyCalc //but using your App's Name
…at the top of your Tests.swift file, and you should have access to all the objects you’ve declared in your app. I suspect that this less exacting method may create slight inefficiencies when your project builds and indexes, but the effect will likely be negligible and you probably won’t even notice a difference.
Setting Up
Looking at your fresh Tests.swift file, you’ll have two override
functions and two example test functions. Notice the diamonds in the gutter next to the example tests (Figure 5). Clicking on the diamond next to a given test (e.g. 5.1) will run that particular test. Similarly, clicking the diamond at the top of the XCTestCase
class (e.g. 5.2) will run all the tests in that class.
For now we can delete the example tests.
First, add the object that you want to test as property named sut
on the test class. For example:
var sut: NumberCruncher!
The name sut
stands for “System Under Test” and is a common naming convention.¹ Being a unit test, we want to keep the scope of the test narrow, and therefore we’re limiting the System Under Test to a single object. Good practice. We’re declaring sut
as an implicitly unwrapped property because, while it doesn’t exist right now — at the creation of the StealthyCalcTests
class, we can guarantee it will exist and be available by the time we use it in any of our tests. If sut
doesn’t exist, we have problems unrelated to the tests themselves and we want the program to crash out anyway so that we know that this is where the problem lies — with the instantiation of the SUT — not with anything we’re unit testing.
To ensure that our sut
property does exist when we run our tests, we instantiate it inside the setUpWithError
override
method after calling super
:
try super.setUpWithError()
sut = NumberCruncher()
And to ensure that it is cleaned up at the end of the XCTestCase
lifecycle, we also should add to tearDownWithError
:
sut = nil
try super.tearDownWithError()
By allocating sut
to and deallocating sut
from memory in this way, we can ensure that each test method that we will write in this class starts with a fresh slate. The sut
property will be allocated before each individual test function and deallocated after each individual test function. Therefore the state of the sut
property in a hypothetical testOne()
will not carry over and affect the state of the sut
property in testTwo()
.² The idea of tests being independent of one another is a key tenet of unit testing.
Actually Writing and Running Tests
Now that you’ve done all the set up, writing the actual test methods themselves is pretty simple. As explained in the introduction article, tests methods contain one or more test assertions (like XCTAssertEqual
on lines 5 and 6 in Figure 6). These assertions are like the most basic building blocks of the XCTest framework as you can’t have a working test method without one. While the other lines in your test methods will serve to set up the conditions to be tested, it’s the assertions that really do the heavy lifting of actually testing your code.
To observe this arrangement in vivo, take a look at Apple’s provided example of a test method:
The first line within the testEmptyTableRowAndColumnCount
method sets up a newly instantiated table object — the conditions to be tested. And the assertions do the work of testing — verifying that the input produces the expected output. In this case, we’re verifying that the rowCount
and columnCount
properties on a new instance of custom type Table
are both 0.
It’s interesting to note that in this example, Apple instantiates the table
object (which is effectively the System Under Test here) directly in the test method itself. This pattern contrasts with the pattern we worked through in the previous section, where we instantiated the SUT in the setUpWithError
method. There is some debate as to which pattern is best, and you can read all about my understanding of the matter in the next post. But for now, you can rest assured that both patterns are viable. If Apple were to follow the pattern from the previous section, the XCTestCase
would look more like this:
There are many different assertions you can use in your tests beyond theXCTAssert
and XCTAssertEqual
assertions that have appeared in these articles so far. The whole list can be found by starting to type “XCTAssert” into a file that imports XCTest
, then scrolling down through the drop down list that appears. Select one of them, to add it to your code, then right-click on it to “Jump to Definition” to learn more about it and see all the others in the documentation.
Finally, take note that the test methods in Figures 6 and 7 start with the “test” prefix that will be detected by Xcode’s XCTest framework. And, as mentioned previously, clicking the diamond (not shown) that will appear next to the method runs the test. If the assertion fails, the corresponding custom message (e.g. “Row count was not zero.”) will print to the console.
With that, you should have enough to cobble together a few unit tests of your own. See the next article to learn more about best practices in writing unit tests and stay tuned for some material on performance tests and UI tests. Or in the meantime, check out this section of a raywenderlich.com tutorial for a quick intro to writing your first UI test and the next section of the same tutorial to get started on performance testing.
A Little Bonus Section on Debugging
So your test failed. You were sure it was going to succeed, but it failed. I guess that’s why we write tests in the first place, but still — darnit. Now you’ll have to search through your code to figure out where it went wrong. Debugging with tests written will mostly be the same process you’re used to without them, but to more quickly get a snapshot of your SUT’s state when the test fails, you can use the Breakpoint navigator to set a Test Failure Breakpoint (Figure 8).
When you run the test with a test failure breakpoint set, the test run will stop at the XCTAssert line that fails and you’ll be able to use the debug console on the bottom to look at your SUT’s state at the moment of failure. From here, you can use your typical debugging process and breakpoints to find the root of the problem.
Taking advantage of the optional String
parameter in the assertion to write a failure message— as shown in lines 5 and 6 in Figure 6 — can be a big help in the debugging process too.
[1] Piper, D. (2021, April 14). iOS Unit Testing and UI Testing Tutorial. raywenderlich.com. https://www.raywenderlich.com/21020457-ios-unit-testing-and-ui-testing-tutorial.
[2] Apple Inc. Understanding Setup and Teardown for Test Methods. Apple Developer Swift Documentation. https://developer.apple.com/documentation/xctest/xctestcase/understanding_setup_and_teardown_for_test_methods. (Accessed 2021, August 23)
[3] Apple Inc. Defining Test Cases and Test Methods. Apple Developer Swift Documentation. https://developer.apple.com/documentation/xctest/xctestcase/understanding_setup_and_teardown_for_test_methods. (Accessed 2021, August 23)