Test Execution
Test execution is a well-supported feature of idb. Supporting xctest run
as a primitive means that idb can be used in automated scenarios such as continuous integration or IDEs.
There are three areas that idb aims to solve on top of Xcode and xcodebuild
:
- Structured Output: idb will output machine readable output when using the
--json
flag. This is often required for CI systems.xcodebuild
now supports the-resultBundlePath
flag, which outputs a plist upon completion, but idb also exposes streaming output and can support custom reporters. - Build Once, Run Many, Run Repeated:
xcodebuild
currently supports "Run Many" with thetest-without-building
command. However, this imposes more of a burden on the user than is necessary. In many cases idb supports building a singular.xctest
bundle andxctest install
ing this. This makes it easier to distribute test bundles against many runtimes and also run the same test bundle with a different runtime environment. For example, idb can run the same bundle with different environment variables, without modifying the build output. This makes it ideal for parameterizing the same test bundle with different data inputs. Or running the same test bundle in an Application or Logic Test context. - Listing of Tests: Given an installed test bundle idb also supports listing of tests within a bundle. This means that the user can peek at what tests cases are in a bundle without parsing sources using
xctest list
- Faster test execution environments: "Logic Test" execution is an optimization that idb can use in a Simulator environment. This means that a test can execute much faster by not being injected into a full Application context. If test bundles are fast to execute, idb doesn't impose a cost of launching an Application.
Installation of Test Bundles
Since iOS Runtimes may be remote from the machine that is executing idb commands, idb needs to support the concept of installation
. This is essentially copying across the test bundle artifacts alongside the runtime using xctest install
.
By doing so, the idb companion backend then has the test bundle binary to execute against when using xctest run
. These bundles are also persisted, which makes caching of test bundles more feasible.
The following shows the installation of a test bundle, listing the available bundles, then the test cases within a installed bundle:
$ idb xctest install Fixtures/Binaries/iOSUnitTestFixture.xctestInstalled: com.facebook.iOSUnitTestFixture$ idb xctest listcom.facebook.iOSUnitTestFixture | iOSUnitTestFixture | x86_64, i386$ idb xctest list-bundle com.facebook.iOSUnitTestFixtureiOSUnitTestFixtureTests/testHostProcessIsMobileSafariiOSUnitTestFixtureTests/testHostProcessIsXctestiOSUnitTestFixtureTests/testIsRunningInIOSAppiOSUnitTestFixtureTests/testIsRunningInMacOSXAppiOSUnitTestFixtureTests/testIsRunningOnIOSiOSUnitTestFixtureTests/testIsRunningOnMacOSXiOSUnitTestFixtureTests/testPossibleCrashingOfHostProcessiOSUnitTestFixtureTests/testPossibleStallingOfHostProcessiOSUnitTestFixtureTests/testWillAlwaysFailiOSUnitTestFixtureTests/testWillAlwaysPass
This makes the process of xctest
management a lot more similar to app management. Using a given test bundle, it's then trivial to execute tests against it
$ idb xctest run logic --json com.facebook.iOSUnitTestFixture{"bundleName": "iOSUnitTestFixtureTests", "className": "iOSUnitTestFixtureTests", "methodName": "testHostProcessIsMobileSafari", "logs": [], "duration": 0.22353005409240723, "passed": false, "crashed": false, "failureInfo": {"message": "(([NSProcessInfo.processInfo.processName isEqualToString:@\"MobileSafari\"]) is true) failed", "file": "/repo_root/iOSUnitTestFixture/iOSUnitTestFixtureTests.m", "line": 50}, "activityLogs": []}{"bundleName": "iOSUnitTestFixtureTests", "className": "iOSUnitTestFixtureTests", "methodName": "testHostProcessIsXctest", "logs": [], "duration": 0.0002950429916381837, "passed": true, "crashed": false, "failureInfo": null, "activityLogs": []}{"bundleName": "iOSUnitTestFixtureTests", "className": "iOSUnitTestFixtureTests", "methodName": "testIsRunningInIOSApp", "logs": [], "duration": 0.0003999471664428711, "passed": false, "crashed": false, "failureInfo": {"message": "(([NSClassFromString(@\"UIApplication\") performSelector:@selector(sharedApplication)]) != nil) failed", "file": "/repo_root/iOSUnitTestFixture/iOSUnitTestFixtureTests.m", "line": 30}, "activityLogs": []}{"bundleName": "iOSUnitTestFixtureTests", "className": "iOSUnitTestFixtureTests", "methodName": "testIsRunningInMacOSXApp", "logs": [], "duration": 0.00030100345611572255, "passed": false, "crashed": false, "failureInfo": {"message": "(([NSClassFromString(@\"NSApplication\") performSelector:@selector(sharedApplication)]) != nil) failed", "file": "/repo_root/iOSUnitTestFixture/iOSUnitTestFixtureTests.m", "line": 40}, "activityLogs": []}{"bundleName": "iOSUnitTestFixtureTests", "className": "iOSUnitTestFixtureTests", "methodName": "testIsRunningOnIOS", "logs": [], "duration": 0.0002809762954711914, "passed": true, "crashed": false, "failureInfo": null, "activityLogs": []}{"bundleName": "iOSUnitTestFixtureTests", "className": "iOSUnitTestFixtureTests", "methodName": "testIsRunningOnMacOSX", "logs": [], "duration": 0.0003479719161987306, "passed": false, "crashed": false, "failureInfo": {"message": "((NSClassFromString(@\"NSView\")) != nil) failed", "file": "/repo_root/iOSUnitTestFixture/iOSUnitTestFixtureTests.m", "line": 35}, "activityLogs": []}{"bundleName": "iOSUnitTestFixtureTests", "className": "iOSUnitTestFixtureTests", "methodName": "testPossibleCrashingOfHostProcess", "logs": [], "duration": 0.0003750324249267579, "passed": true, "crashed": false, "failureInfo": null, "activityLogs": []}{"bundleName": "iOSUnitTestFixtureTests", "className": "iOSUnitTestFixtureTests", "methodName": "testPossibleStallingOfHostProcess", "logs": [], "duration": 0.00037300586700439453, "passed": true, "crashed": false, "failureInfo": null, "activityLogs": []}{"bundleName": "iOSUnitTestFixtureTests", "className": "iOSUnitTestFixtureTests", "methodName": "testWillAlwaysFail", "logs": [], "duration": 0.0002980232238769531, "passed": false, "crashed": false, "failureInfo": {"message": "failed - This always fails", "file": "/repo_root/iOSUnitTestFixture/iOSUnitTestFixtureTests.m", "line": 76}, "activityLogs": []}{"bundleName": "iOSUnitTestFixtureTests", "className": "iOSUnitTestFixtureTests", "methodName": "testWillAlwaysPass", "logs": [], "duration": 0.00024402141571044914, "passed": true, "crashed": false, "failureInfo": null, "activityLogs": []}
What is a Test Bundle?
An xctest
bundle is fundamentally a Dynamically Linked Framework that contains executable test code.
xctest
bundles are loaded at runtime by the process that they are being injected into, just like a plugin or shim. The dynamic library within the .xctest
bundle links with XCTest.framework
and can also link with other System-Level Frameworks, or code that is statically linked inside the test bundle.
Since these bundles aren't substantially different to any other Framework or dynamic library, they can be relocated onto another machine, or amongst many iOS runtimes on the current machine.
iOS Simulators
Simulators are a common place to execute test bundles, especially when testing unit or integration level functionality that may not have dependencies on running a physical device.
The performance differences between a Simulator and Device are also less relevant if you only care about correctness. Simulators are often easier to manage within a CI environment.
There are a number of test execution environments that idb supports:
idb xctest run logic
Logic Tests: A Test Bundle that can run in the context of any process, typically used for Unit Tests. These kinds of xctest
bundles do not typically require a specific host Application as all code-under-test.
Like all xctest
bundles, they can dynamically link the code-under-test at runtime, or statically links the tested code inside the Test Bundle's dynamic library.
Logic tests are typically injected into a small host process like the xctest
commandline. macOS has a xctest
commandline at /usr/bin/xctest/
, the iOS Simulator xctest
executable is located at $DEVELOPER_DIR/Platforms/iPhoneSimulator.platform/Developer/Library/Xcode/Agents/xctest
.
The xctest
commandline will load the xctest
bundle provided as an argument and run tests according to the arguments, or from an XCTestConfigurationFilePath
provided as an environment variable passed to the process.
idb then injects a shim into the xctest
process and hooks calls to XCTest.framework
methods. This is then used to report results back to idb. This communication happens via a FIFO, passed into the spawned xctest
process.
This is not a mode of execution that is supported by Xcode & xcodebuild
. However, if you're wanting to optimize the performance of your test executions, for instance to get faster signal on code changes, then this is a worthy optimization. The main reason for not using "Logic Tests" is if the Test code or the code-under-test requires an application context, for instance if it's using UIApplication
. As such, Logic Tests are often well suited to business or domain logic that doesn't depend on much more than Foundation, or your own libraries.
idb xctest run app
Application Tests: Application Tests are similar to Logic Tests, but they run inside an iOS App Host Process. This is typically used for Unit & Integration tests that depend on an Application's code, or running inside the context of an Application.
These tests can manipulate the UI within an Application, depend certain behaviours of UIKit
, or interact with the Application Delegate. They require a little more work to bootstrap, because of the arbitration between idb and the host process.
idb uses XCTestBootstrap
which understands how to perform this arbitration and translate delegate callbacks into machine-readable output.
idb xctest run ui
UI Tests: In Xcode 7, Apple deprecated UIAutomation
and replaced it with UI Testing support inside the XCTest
framework itself. This means that UI Tests have a similar execution model to Application Tests, by injecting into an Application Process.
As part of the security model of running these tests, the Test Bundle cannot manipulate the User Interface un-aided. The bundle running inside the application process coordinates with Xcode via a daemon process called testmanagerd
.
This mediated connection between the injected application process and IDE Host also allows for test results to be delivered across a delegate protocol to the IDE Host.
idb and Shims
In order for idb to extract structured output from a test process, it uses a "shim": a dynamically linked library that can interpose some of the functionality of XCTest.framework
.
The default installation instructions for idb will create shims for you. If you're re-locating the idb_companion
you should take a look at this class, which describes how idb finds these binaries.
fbxctest
?
What about fbxctest
was built as a drop-in replacement for xctool
before it. idb doesn't have the same command-line interface for test execution, so you will need to invoke idb differently to use it.
fbxctest
will remain around for the foreseeable future, as both idb and fbxctest
depend on the same underlying XCTestBootstrap
framework. fbxctest
may be deprecated in the future.