Playgrounds: Speed up your tests feedback in Swift
Playgrounds was released in September, 2016. It’s a powerful environment integrated in Xcode to code in real-time in Swift. Using Playgrounds is a great way to learn, experiment, and quickly prototype.
This is particularly interesting when comparing its speed with an iOS or macOS project. Compiling such a project takes tens of seconds or even several minutes. Running tests is therefore also slowed down.
Test-Driven Development is about getting feedback quickly. Wouldn’t it be interesting to get real-time results? Let’s see how we can leverage Playgrounds to write and run unit tests in Swift.
Writing unit tests
XCTest, the Apple framework for unit testing, is available through Playgrounds. Writing unit tests in Playgrounds is exactly similar to writing them inside a project with a unit test target. You just have to import XCTest
and to write unit tests inside a test case class:
import XCTest
class HelloServiceTests: XCTestCase {
func testMakeMessage() {
// Arrange
let now = 1587301244
let dateProvider = FakeDateProvider(now: now)
let service = HelloService(dateProvider: dateProvider)
// Act
let message = service.makeMessage()
// Assert
XCTAssertEqual(message, "1587301244 - Hello.")
}
}
It does not change your habits. You write a unit test, instantiate a class, call a method and assert a result as you are used to. You can add as many tests and test cases as you want. See? Simple.
Running unit tests
The difference with a classic unit test target starts here, but that does not mean it will be complicated. Rather, it’s very simple. The solution fits in one line of code. Here it is:
HelloServiceTests.defaultTestSuite.run()
Results of unit tests will appear in the console. These are the same logs you can find in Xcode. Each time you are changing the code, unit tests will be automatically launched again and new logs will be printed.
Test Suite ‘HelloServiceTests’ started at 2020–04–30 16:31:24.776
Test Case ‘-[HelloServiceTests testMakeMessage]’ started.
Test Case ‘-[HelloServiceTests testMakeMessage]’ passed (0.017 seconds).
Test Suite ‘HelloServiceTests’ passed at 2020–04–30 16:31:24.794.
Executed 1 test, with 0 failures (0 unexpected) in 0.017 (0.018) seconds
But, why do we need this line? With an Xcode project, to run a test, we are usually pressing Cmd + U
or choosing Product > Test
from the menu. Behind the scene, the test target gathers every test case and test method while we are creating them. This list is then given to the test runner which will execute every test listed.
In a playground, this process does not exist really. We have to manually create a test suite (something containing all of the tests) and run it. If you want to know more about the behaviors of unit testing frameworks, I recommend you to take a look at xUnitPatterns.com.
Having multiple XCTestCase
Previously, I told you that you can write as many test cases as you want. So if you want to run all of thems, you will have to create and run all test suites. One way to do that is to call the run
method on each default test suite. But there is a better way:
let globalSuite = XCTestSuite(name: "Global - All tests")
globalSuite.addTest(HelloServiceTests.defaultTestSuite)
globalSuite.addTest(FooTests.defaultTestSuite)
globalSuite.run()
The main advantage of creating a global test suite is to have numbers that aggregate all the cases. You will have results and failures for each test suite, and at the end, the global count.
Test Suite ‘Global — All tests’ passed at 2020–04–30 16:31:24.794.
Executed 3 tests, with 0 failures (0 unexpected) in 0.018 (0.028) seconds
Adding a suite to a suite is possible because everything is XCTest
: XCTestCase
and XCTestSuite
inherit from XCTest
. If you have multiple unit test targets in your project, you will see that your tests are grouped per class and target.
Improving errors
Reading errors through a long log isn’t the most attractive way to work in Test-Driven Development. There aren’t those little red or green badges next to every test. To improve our experience, we can use XCTestObservationCenter
to observe the progression of test runs through an object implementing XCTestObservation
.
class TestObserver: NSObject, XCTestObservation {
func testCase(_ testCase: XCTestCase,
didFailWithDescription description: String,
inFile filePath: String?,
atLine lineNumber: Int) {
// Do something here.
}
}
let testObserver = TestObserver()
XCTestObservationCenter.shared.addTestObserver(testObserver)
Multiple methods are responding to different events. The one in the example is called when a test case reports a failure. This is an opportunity to improve feedback.
We can add an assertionFailure
, for example. In a playground, this statement will stop the execution. Because the log is written progressively, the last message will be our error message.
Test Case ‘-[HelloServiceTests testMakeMessage]’ started.
MyPlayground.playground:26: error: -[HelloServiceTests testMakeMessage] : XCTAssertEqual failed: (“15873012445 — Hello.”) is not equal to (“1587301244 — Hello.”)
Fatal error: XCTAssertEqual failed: (“15873012445 — Hello.”) is not equal to (“1587301244 — Hello.”): file MyPlayground.playground, line 26
We can also code our data structure and have a better print, or whatever other behavior we want. And why not a live view?
Organizing files
As I see Playgrounds, it’s meant for easily and quickly prototype and try things. It can be difficult to make a big project with it, mainly because of the file management and the tree structure. However, we don’t have to do everything in one file.
Having multiple files
The Navigator, the left panel of Xcode and Playgrounds, is often hidden. Well, it’s understandable: we code almost all the time only in the main window and file. If you take a look at the Navigator, you can see a Sources
folder.
Here, you can add every file we want. The Sources
folder is considered as a different module by Playgrounds. It means two things. First, you need to explicitly set access levels to public
on every object, method, or property you want to access. In a Swift module, the default access level is internal
.
Second, the module is compiled when a change occurs in it, and only inside it. It’s a big gain of time. Editing code in the main file of the Playgrounds does not affect the module. It is why we can keep a really quick feedback.
Conclusion
It is easy to write unit tests and to run them in Playgrounds, and we are using tools that we already know. Playgrounds adds a very appreciable real-time execution for doing Test-Driven Development. The responsiveness of the feedback is really great, it’s a big advantage for the red-green-refactor cycle.
This is not perfect for handling multiple files of a big project, or displaying errors, but we have solutions to improve our experience. It’s not a big problem to work, to prototype, to explore.
In future articles, I will tell you about other ways to speed-up Test-Driven Development in Swift. We will see how we can use Swift Package or frameworks to help us. Keep in touch :)