Sasha Goldshtein Sasha Goldshtein - 2 months ago 12x
iOS Question

Using Xcode UI tests to test underlying framework behavior

I am building an iOS framework that collects information about various events in an iOS app and performs local and remote analysis. Some of these events can't be tested outside of an app: for example, view controller transitions. To test these events, I built a test iOS app and would like to use Xcode UI tests to:

  • Initiate a view controller transition by, say, tapping a button that pushes a VC to a navigation controller, or by switching to a different tab in a tab controller

  • Verify that the framework was able to detect the view controller transitions and generate the necessary events (e.g., send them to the server)

The problem is that Xcode UI tests run outside the app, so I can't access any shared objects to verify that the framework is functioning properly. I also can't insert any mock objects, because, again, I don't have access to the framework loaded in the app's process.

I went as far as trying to load the framework in a kind of test mode and have it update a label with some results that the UI test would then be able to read. But even that is difficult, because
doesn't give me the actual text of the element; I can only query for elements with some predefined text. I can sort of work with that, but it seems like a very roundabout approach for something that should be rather trivial.

Are there any better alternatives for testing events that can only be simulated in a running app, and then verifying that my framework was able to detect and process them? In addition to view controller transitions, I am also interested in touch interactions, accelerometer events, and other features that can't be simulated outside of an app. Naturally, I can simulate these events for unit testing, but I would also like to automatically test that when these events occur in an actual app, the proper responses are generated in my framework.


You could try using SBTUITestTunnel. This library extends the functionality of UI Tests adding some features that might come in handy in cases like yours.

Network monitoring

The library allows your test target to collect network calls invoked in the app. Usage is pretty straight forward:

func testThatNetworkAfterEvent() {
  // Use SBTUITunneledApplication instead of XCUIApplication
  let app = SBTUITunneledApplication()
  app.launchTunnelWithOptions([SBTUITunneledApplicationLaunchOptionResetFilesystem]) {
      // do additional setup before the app launches
      // i.e. prepare stub request, start monitoring requests

  app.monitorRequestsWithRegex("(.*)myserver(.*)") // monitor all requests containing myserver

  // 1. Interact with UI tapping elements that generate your events

  // 2. Wait for events to be sent. This could be determined from the UI (a UIActivitiIndicator somewhere in your app?) or ultimately if you have no other option with an NSThread.sleepfortimeinterval

  // 3. Once ready flush calls and get the list of requests
  let requests: [SBTMonitoredNetworkRequest] = app.monitoredRequestsFlushAll()

  for request in requests {
      let requestBody = request.request!.HTTPBody // HTTP Body in POST request?
      let responseJSON = request.responseJSON
      let requestTime = request.requestTime // How long did the request take?

The nice thing of the network monitoring is that all the testing code and logic is contained in your test target.

Custom block of code

There are other use cases where you need to perform custom code to be conveniently invoked in the test target, you can do that as well.

Register a block of code in the app target

SBTUITestTunnelServer.registerCustomCommandNamed("myCustomCommandKey") {
    injectedObject in
    // this block will be invoked from app.performCustomCommandNamed()

    return "any object you like"

and invoke it from the test target

func testThatNetworkAfterEvent() {
  let app = ....

  // at the right time
  let objFromBlock = app.performCustomCommandNamed("myCustomCommand", object: someObjectToInject)

Currently you can only inject data from the test target -> app target. If you need the other way around, you could store that data in the NSUserDefaults and fetch it using the SBTUIApplication's userDefaultsObjectForKey() method.

I personally don't like the idea of mixing standard and test code in your app's target, so I'd advise to use this only when really needed.


I've update the library and starting from vision 0.9.23 you can now pass back any object from the block to the test target. No need for workarounds anymore!