What's New in Testing

Session 409 WWDC 2017

Xcode 9 has new APIs for structuring your test logging and including your own attachments and screenshots, as well as new support for parallel device and simulator testing. Learn how to write UI tests that target multiple applications, and find out ways to improve the performance of your UI tests.

[ Applause ]

Good afternoon and welcome to what's new in testing.

My name is Wil and I work on Xcode and on XCTest.

So, what is new in testing?

The answer is a lot.

In fact, so much that we don't have time to do all of the justice here today, so let's get started.

First off, we have a bunch of enhancements in both Xcode 8.3 and Xcode 9.

I'm going to just call these out and then I'm going to let you read up on them in the reference documentation for XCTest which has just gotten a major overhaul.

Then we'll take a look at new APIs for asynchronous testing followed by multi-app testing and some great performance improvements in UI testing.

Finally, we'll wrap things up with a group of technologies called activities, attachments, and screenshots.

Let's look at those enhancements.

So, in Xcode 8.3, we added UI testing for Siri intents with the new XCUISiriService.

We also introduced UI testing support for the Touch Bar on macOS.

In XCTest for Xcode 9 we have refined the framework Swift interface as part of the Swift 4 effort and we've also added a new block-based teardown API that lets you structure context specific teardown in your test methods without additional state or properties in your test classes.

In UI testing we've introduced a new element type for the macOS menu bar items.

In addition, there's new API on XCUI element that waits on the existence of an element reducing the need for sleep or other artificial delays in your tests.

[ Applause ]

Xcodebuild now launches tests directly via core simulator so you'll no longer see the simulator app launch when running tests from the command line.

We're also very excited to announce Xcodebuild now supports parallel device testing.

[ Applause ]

This means if you pass multiple destination specifiers to Xcodebuild it will build once for all specifiers sharing the same platform and then run the test for those destinations simultaneously.

This should give a huge speedup to many continuous integration scenarios.

On the localization side, in Xcode 9 you can set the language and region for your test to run in.

This scheme option allows you to easily test many different localizations for your projects.

I also want to take a moment to call your attention to some improvements in Xcode Server.

So first of all, as of Xcode 9 you no longer need macOS server, you can turn on Xcode Server for your team with just the flick of a switch by going to the new preferences pane directly in Xcode.

Xcode Server also has an improved provisioning workflow and adopts the Xcodebuild improvements for CoreSimulator and parallel testing.

And those per scheme localization support automatically extends to all of your bots.

So that is our whirlwind tour of the most exciting new enhancements.

Now let's slow down a bit and take a closer look at asynchronous testing.

Async testing allows you to validate code which doesn't finish immediately, but instead calls back later with closures, delegate methods or other delayed completions.

This includes tasks like opening documents, work done on background threads, communicating with other processes, network activity, animations, and a range of UI testing scenarios.

We introduced APIs for async testing in Xcode 6 several years ago now.

These methods on XCTestCase let you define conditions or expectations as we call them and then have your test wait for them to complete or be fulfilled.

So, here's a simple example.

This fragment of test code opens a document, waits for it to finish, and asserts that it was successful.

The expectation object is created before the document is opened.

The test waits for it below the call to open and fulfills it inside the closure which allows the test to then continue on and execute the test code after the call to wait.

But this is the original API that we had in Xcode 6.

It works well enough, but it has some limitations.

First, timeout string waiting are always treated as test failures.

Second, waiting requires the test object itself which makes it hard to factor out into any kind of test library or support code.

It's also not possible to have what we call nested waiting.

Nested waiting involves additional groups of expectations that get waited on inside an outer weight context.

To solve these problems, we've introduced a new class XCTWaiter.

This class extracts the logic of waiting that used to be contained in XCTestCase and allows you to explicitly declare the expectations you're waiting on.

Timeouts and other events are handled through a delegate API and are also returned from the wait API as a result value.

This provides considerably more flexibility in how you can structure your asynchronous tests.

If we return to the previous example, let's take a look at what the options are now for waiting.

In the original code, note that no expectations are mentioned.

This is because the implementation is implicit, it waits on all active expectations the test object has created.

With just a small change now the test is waiting explicitly on the document expectation.

Now another equivalent way of constructing this is to create an instance of XCTWaiter with a test case as delegate.

And yet another option is to wait using a class method on XCTWaiter and then handle the result value.

This flexibility makes it really easy to use XCTWaiter in helper method, nested contexts or in test library code that's completely decoupled from your test cases.

Now in addition, to creating XCTWaiter we've also expanded the API for XCTTestExpectation.

The initializer is now public API which also decouples the creation of expectations from XCTestCase.

We've also introduced an expected fulfillment count property, this is where expectations representing conditions which occur multiple times.

Now for conditions which should not occur at all there's an inverted API for expectations which will cause the waiter to raise a failure only if the expectation is fulfilled before the timeout collapses.

Finally, XCTWaiter allows you to enforce the order in which expectations are fulfilled using an optional flag with the wait APIs.

So, those are the updates to asynchronous testing, XCTWaiter, our new API for managing expectations and a bunch of improvements to XCTest expectation.

Both of these classes are fully decoupled from XCTestCase giving you a much easier and more powerful system to use.

Now I'd like to talk about UI testing and in particular, UI testing with multiple applications.

The starting point of almost every UI test is an XCUIApplication instance.

This class lets you launch and terminate the app you're testing, as well as create queries for finding the user interface elements which you then automate by sending synthetic events.

UI testing has a target application concept which is the application your tests are primarily testing.

In your project settings for your tests you designate an app in the same project as being the target application.

This enables you to call the default initializer for XCUIApplication and the instance will be created with the information for installing, launching and interacting with that application.

This mechanism is very convenient, but it doesn't give you a way to test other applications.

Some examples of what we would call multi-app scenarios include app groups where you have more than one application in your project and they somehow interact together and pass data back and forth.

Another example would be settings if you need to automate the settings for your app and change preferences for different test scenarios.

And yet another example, our app extensions.

All of these require more than a single target application.

To solve this, we've added some new APIs to XCUIApplication.

First, there are initializers that allow you to specify an app by its bundle ID or alternatively on macOS you can also use a file URL to designate the location of the app on disk.

Second, we've added an activate method that will bring the app from the background to the foreground if it's already running and launch a new instance if it's not.

But launch API would terminate any previous running instance first, so you always have a cleaner slate when you start your test.

Activate is useful for those scenarios when you're not interested in wiping out previous state, but you actually want to resume from some earlier point in the test.

Finally, there's a new state property that you can use to monitor changes in the applications you're testing.

So, here's some example of using these APIs.

This code creates two XCUIApplication instances with bundle IDs.

This is a pair of apps perhaps in an app group.

After launching and interacting the readerApp the test then launches the writerApp and finally, after some more interaction it uses the activate API to bring the readerApp back to the foreground without terminating it first.

So now let's see that in action, here's Warren Ma with a demo of multi-app UI testing.

[ Applause ]

Thanks Wil.

So as Wil just described, now in Xcode 9 you can write UI tests that involve multiple applications and I'm excited to show you guys how you might incorporate the new APIs into your own test suites.

So, let me go ahead and start by showing you these two related apps that I have here.

I'm running them on a development device and I'm using QuickTime to show you what's on the screen.

The first one is a message posting app, the writerApp.

Here we have a username, we have a text view that we can tap into, we can type out a test message, tap return on the keyboard and post it to the server by tapping on the Send button.

Great, so it's been posted.

Now how do we view these messages?

Well that's the purpose of our second app the readerApp.

Here is a list of all messages that have been posted to the server and at the very top is the most recent message posted the one I just sent and we can tap into that to view it in detail.

And then we can return to that list of messages using the button at the top left All Messages.

So, let's say you want to test this process of typing out a test message in the writerApp, posing it to the server and then verifying that it appears at the top of the list of messages from the second application.

Well before Xcode 9 you would have to write separate UI tests for each application and even then, you wouldn't really have a way of verifying the behavior between both.

But now with multi-app testing in Xcode 9, multi-UI testing in Xcode 9 we can easily test real-world scenarios between multiple applications.

To show you how that looks let's write a quick UI test to automate the workflow I just demonstrated.

All right, the first step in our test is to launch our Reader application.

In order to do that we're going to initialize it using its bundle ID.

Afterwards we can go ahead and launch it from a clean state.

Once our readerApp has been launched we're just going to go ahead and verify the first message in that list of messages.

To do that we'll tap into the first message in the list, we'll verify that its contents are what we expect and finally, we'll return to the list of messages using that button at the top left.

Once we verified the first message in the list we want to type out our test message and post it to the server into the writerApp.

To do that the first step once again is to initialize the writerApp using its bundle ID, in this case it's com.mycompany.Writer.

Afterwards, it's as easy as just calling Activate.

Now Activate also waits for the application state to become running foreground before returning, so we don't need to do any sort of manual waiting on the application state.

Once Activate has returned we know that the test is good to continue.

Once the writerApp is running foreground we can go ahead and compose our test message and post it to the server.

To do that we'll tap into the text field, we'll type out our test message, we'll tap on the return button on the keyboard, and then we'll tap on the send button to post it to the server.

Now we want to make sure that it appears on the top of the list of the messages in the readerApp.

Earlier we used Activate to switch between the two applications, but in this case, let's make use of that back to app button that's built into iOS which appears at the top left corner of the screen.

So, to do that we're going to tap on the back to app button.

However, in this case because we're not using Activate we do need to manually wait on the application state to become running foreground before continuing with the test.

So, to do that let's make use of the new predicate based expectation API.

So, we want to make sure that the Reader app state has become running foreground before returning.

After we've defined our expectation we'll go ahead and wait for that expectation to become true with a timeout of 10 seconds which should be more than sufficient.

Once the Reader app has become running foreground we can go ahead and continue with the final step of our test which is to verify that the message appears at the top of the list.

Similar to what we did before, we'll tape into the first message in the list, we'll verify that its contents are the message that we just posted, and then we'll return to the list of messages.

So, let's go ahead and run the test to see it in action.

To do that I'll click on the test diamond in the left-hand gutter.

So, when you run UI tests in general it has to install what is called the UI test runner and that is the test process in which UI tests run in addition to installing the app.

So, it just takes a bit of time for that to launch.

Once it's launched the test can go ahead and continue to run.

So once again, the first step of our test was to launch the Reader app and verify the first message in the list of messages.

Then we use Activate to open the Writer app, type out our test message and post it to the server.

And we use the back to app button to switch back to the Reader app and verify that the message appeared at the top the list.

So [inaudible] how easy it is to make use of the awesome new multi-UI testing APIs available in Xcode 9 to test multiple applications.

And with that I'd like to hand it back to Wil.

[ Applause ]

Thanks Warren, that's really great stuff.

With just a few additions the realm of UI testing possibilities has gotten much broader.

The new initializers let you test any application and the Activate API means you can switch between multiple applications without restarting them, multi-app UI testing.

So, let's switch gears and talk about performance in UI testing.

At the heart of UI testing are the user interface elements that your test will want to interact with.

These are the buttons, labels, table views, etcetera.

The UI tests create queries to describe how elements are found.

For example, here a button element has the query app.navigationBars.buttons with the subscript of done.

That means that the element has the type of a button, the label or title of done and can be found in a navigation bar.

So how do these queries work?

Queries use accessibility data, the same semantic information used by VoiceOver technologies as a kind of searchable structure for the application.

To evaluate a query the test process requests what we call a snapshot of the current data.

This request is sent from the test process to the application so these are separate processes, we're using interprocess communication here.

The request is sent from the test process to the application, now the application captures the snapshot data, serializes it and transmits it back to the test process.

Once the test process has unpacked the data it can evaluate the query by searching through the snapshot.

Snapshotting works and because of how it works it takes a single atomic representation of the state of the UI at that moment, but it introduces some performance challenges.

If we consider two axes of performance, time on one hand and memory on the other, snapshots have potential problems with both.

Snapshots that take too long for apps that have many UI elements, for example tables with thousands of rows, large collection views these will cause timeouts that in turn trigger test failures.

If the snapshot data is too large memory pressure on the system may also result in processes being terminated.

So, these challenges led us to the question of how can we improve snapshot performance.

And we came up with several approaches.

First of all, we wanted to cut down on the overhead of transporting all this data between processes.

To do that we implemented what we are calling remote queries.

With remote queries, instead of the test process requesting the snapshot it actually transmits the query itself, which is a very small amount of data.

The app will still create a snapshot, but instead of transporting it anywhere it simply evaluates the query right there in process.

And at the end, it sends back just the results, again a tiny amount of data, to test process.

So how does all this perform?

Well remote query performance turned out to speed things up by as much as 20% and reduce memory spiking by about 30%.

Now this was a good start, but it was nowhere near what we had in mind to achieve.

So that brought us to our second optimization which is query analysis.

Our goal here was to reduce the size of the snapshot itself, to simply collect less data.

Snapshot uses a fixed set of accessibility attributes, but we determined by analyzing your queries we could identify a minimal set of attributes, roughly half the full set for many common queries, greatly reducing the amount of data collected.

Other properties that you might need for assertions afterwards could still be fetched on demand for specific elements, but the snapshot itself will now be much slimmer.

So, query analysis turned out to be an even better performance optimization, roughly 50% faster in the common cases and having a memory high watermark around 35% lower.

So things are looking pretty good, but we wanted to step back and see if by really getting outside the box we could reimagine the system in a way that would transform things.

That brought us to the idea of eliminating snapshots entirely.

So how could we do that, snapshots gave us this guarantee of an atomic representation of the state of the UI?

Well it turns out because of the remote query infrastructure we now have that just because we were already running our query in the process of the application.

So, where traditional queries work by examining the snapshot data exhaustively we have introduced an API called first match that tells the query stop as soon as you find the first thing that matches because many times it's excessive to search all through the data to find every possible match because you can specify elements with precision that really makes them unique.

So, first match causes the query to return early and you can add it to any query that you already have.

Here's an example where first match has been added to a query we looked at earlier.

And if you imagine that the app we were testing was one with a navigation bar and a table view and the table had thousands of rows in it, traditional evaluation would've examined every single row in the table even though we're looking for a button in the navigation bar.

But first match allows us to stop as soon as we find that button and we never look at a single row in that table.

So how does first match perform?

Well it's safe to say first match is a game changer.

For many queries, it's as much as an order of magnitude faster and eliminates memory spiking entirely.

[ Applause ]

Now the performance improvements in first match are fantastic, but it is important to notice the difference between traditional queries and not simply litter first match throughout your code without some consideration.

Traditional query evaluation finds all matches, this helps detect ambiguous queries because will raise a failure if you attempt to interact with an element that has multiple matches.

First match is removing that protection, your test will get the first match.

And if something in your app's UI has changed so that query is actually not particularly precise and results would be a non-unique result first match won't protect you and the results could be surprising.

So, let's take a look at some example queries and consider whether they're good candidates for first match.

So app.buttons.firstMatch is not a good idea, this is like going into the grocery store and just saying I want food.

You might get a frozen chicken or a piece of bubblegum or banana there's no telling right.

So, this kind of query is simply not precise enough for first match.

Now this is a little better because we've added an identifying string to it as well.

So many apps this might be sufficient right there.

But taking it a step further and adding more precision, more level of detail to the query makes it a better candidate for first match and more likely to just be robust everywhere while still giving you the performance improvements that come with first match.

So while talking about all these optimizations we need to tell you a little bit about block-based NSPredicates and using them in your queries.

Unfortunately, they are at odds with these optimizations, they effectively inhibit them.

That is because first of all, blocks can't be serialized, you can't pass them around between processes so that means no remote query and no first match.

The other problem is that we can't introspect, we can't look into a block at runtime and no which attributes your query actually needs.

So that means the reduced snapshot performance improvements are also off the table when you're using block-based NSPredicates.

Fortunately, the use of block-based NSPredicates in UI testing queries is relatively rare and can almost always be replaced with a format string or an NSExpression based predicate instead.

Now if you find yourself with a case that does require a block-based predicate, I mean they're still supported, they still work or even something where it's much more convenient to use one.

We'd like to know about it, we'd like you to file a bug so that we can give you an API that does the same thing, but in a way that works well with the query optimization.

So, we'd like to hear from you if you feel you have cases where you need a block-based predicate.

That wraps up our discussion of UI testing performance improvements.

Queries should be all around faster in Xcode 9.

Some of the improvements, the remote query and first match do also require the newest OS as the newest macOS, iOS and tvOS.

But the query analysis benefits should work even on older OS's.

So, UI testing performance we've done a lot of work here and we look forward to seeing your tests running faster.

So finally, I'd like to share with you a new group of technologists called activities, attachments and screenshots.

So, let's start with activities which are a new way to create additional structure and longer running UI and integration tests.

There's a single API that lets you group together sections of code by wrapping them in closures passed to a new class XCTContext.

Now here's part of the test report from Warren's demo earlier, which we didn't get a chance to look at, but here it is now.

And that test wasn't particularly long or complex, but you can see there's quite a lot going on here.

So, consider these four actions.

Let's look at the code that caused them.

We have a query to find the view, we tap on it, type some text and interact with a few buttons.

This code makes a good logical grouping, it's the code that composes the any good coffee places message.

So, creating an XCTActivity for this code is very simple.

We simply wrap it in this run activity call, we give it a nice label because that's what we'll show in the test report.

Now let's look at how that changes the test report.

We have the original four activities now enclosed in this new activity compose coffee message and you can expand it still and see the original, more granular details underneath.

But for the high level first pass when you're looking at your test report things will now be more concise and semantically meaningful.

Using this API throughout your longer running tests and in helper methods will just make the test reports much easier to explore.

Now along with activities we've introduced something called attachments.

For a long time now we've wanted the ability to attach richer data to test reports.

The primary motivation is to make it easier to triage failures with additional logs or other data that give insight into the conditions at the time of failure.

In addition, this could be used to support various postprocessing workflows.

For example, sitting down with your designers and looking at screenshots together.

Now attachment support any kind of binary data with convenience APIs for strings, property lists, codable objects, files and images.

Any of these types can easily be attached to your test so when you look at the test report there that data is.

And that brings us to the third edition in our group of technologies which are screenshots.

Many of you have been asking us over the years for an API to explicitly capture screenshots on demand.

Well we're happy to say that here it is, we hope you enjoy it.

[ Applause ]

This new XCUI screenshot providing protocol is implemented by both XCUIElement and a new class XCUIScreen.

So, when you use an element and you capture a screenshot it'll be clipped to just the frame of that element.

So, a button, you'll just see the button, a window you'll see the full window and so forth.

And if you use the screen API you'll get the full screen regardless of what application or applications are present on it.

Now with attachments and screenshots, some of you may be wondering how quickly will this fill up my hard drive.

Well the default policies for attachments and also for screenshots that are captured automatically during UI testing is that if your test passes we delete these for you.

The assumption is in the common case you don't need them, but if your test fails they're present for you.

Now you can override this policy in your scheme.

In the same part of the UI where that localization control exists you can tell us whether you want screenshots captured at all automatically and whether to delete these and attachments when tests succeed or not.

There is also API on the attachment class that lets you on a per instance basis say keep this, don't keep that, and that sort of thing.

So, you'll see that in the next demo with attachments and activities and screenshots in action.

Please welcome Honza Dvorsky to the stage.

[ Applause ]

Thank you Wil.

Good afternoon, my name is Honza and today I'd like to show you how we can organize your tests using activities, then how to screenshot your UI with the new screenshot API, and finally how to attach arbitrary data with your tests.

So, we'll start by looking at the test report of Warren's test.

We can do that by Control-clicking on the test diamond and selecting [inaudible] the report.

When we disclose the test, we reveal the test to transcript.

The test transcript contains all the details about our test, but when it gets longer it can be difficult to navigate.

In addition, what we see here are these leveled steps like these tabs and swipes, but it would help us to organize our test around these higher-level tasks like launch the app or compose in send a new message.

So, we'll use activities to do just that.

We'll jump back to the source and look for good candidates to wrap in our first activity.

For example, this piece of code, compose and send a new message takes care of that.

So, we wrap it in an activity by calling XCTContext.runActivity.

It takes two parameters, the first is the name of the activity, compose and send a new message in our case, and the second is the block, the block represents the scope of the activity.

But we'll close it down here and that's it, that's all you need to do to wrap a piece of code in an activity.

Now I'll sprinkle more activities around this test.

And we'll rerun the test to see how the test report changed.

You can see that I wrapped the launching of the Reader app, the verification of the first message, the activation of the Writer app, the composing and sending a new message and so on.

Now this is the same exact test that Warren wrote here, but this time the activities and the names we give them are getting included in the output as first-class citizens.

So, we send our message, verify that it's the right one, and we're done.

So, let's go back to the test report now.

When we reveal the transcript now we can see it's much shorter and in addition, it better describes what our test does.

It launches the Reader app, verifies the first message, activates the Writer app, composes and sends a new message and so on.

But if you still need to know all the details about that activity all the sub activities are hidden one level deeper.

So, activities are a great way to organize your tests.

Now let's switch gears a little bit.

Our designers that helped us with our app wanted to make sure that our message cells here follow their beautiful specification exactly.

So, what we'll do is write a UI test which captures the visual state of the app and then we'll run this test every night on our bots.

This way, our designers can come in and see what the app looks like anytime they want.

So, this is a new class visual validation tests.

I already have the code to launch the app in the state I want it in and I also have this empty activity gather screenshots and this is where we'll place our screenshots now.

We'll capture two screenshots, the first of the full-screen of the app and one of just the first message cell.

So, to capture the full screen we use the new XCUIScreen API to get a handle to the main screen.

Now as Wil mentioned, XCUIScreen conforms to XCUI screenshot providing so we can just simply ask it for a screenshot of itself.

Now we have the screenshot in memory and we somehow want to persist it with our test and this is where attachments come in.

Attachments can hold any data and XCTAttachment provides convenience initializers for types like strings, files, images and screenshots.

So, we'll create a new attachment to hold our screenshots.

Now as Wil mentioned, the attachments get deleted whenever the test passes, it's a default behavior.

But in our case here we want to persist these attachments regardless of the test result.

We can do a separate attachment by customizing its lifetime to keep always.

And finally, we add this attachment to an activity, we'll add it to this activity that represents our gather screenshots.

Now we captured the full screen, we also wanted to capture just the first message cell.

So, we first used the existing UI testing API to get an XCUIElement for the cell.

An XCUIElement also conforms to XCUI screenshot providing so we can just follow the same steps here.

We ask the cell for a screenshot of itself, we create an attachment for it, customize its lifetime and add the attachment to the activity.

So, let's run the test now and see what we get.

This is a simple test that just launches our app and captures the two screenshots, adds those two screenshots as attachments and finishes.

That's it.

So, we go to the test report.

Now when we reveal the test we see our gather screenshots activity and it contains our two attachments and [inaudible] activity.

So, this is the attachment that represents the first main screen screenshot and this represents the screenshot of just the message cell.

We could use this QuickLook icon here to get a preview window for it, but in fact I want to use this the Assistant Editor to do that.

So, we just select the right screenshot and this is our full-screen screenshot and this is the screenshot of just the first message cell.

So, now you can see how super easy it is to capture, oh thanks.

[ Applause ]

Now you can see how easy it is to really capture any screen or element you have in your app with the new screenshot API and attach it to the test with the new attachments API.

I really hope you give it a try.

Okay and with that I would like to invite Wil back to wrap things up for us.

Wil.

[ Applause ]

Thanks Honza, that is pretty awesome stuff, you can see just how easy activities make it to improve the test structure and capturing rich data with attachments will make fixing your test failures simpler than ever and screenshots, I mean who doesn't love screenshots.

Activities, attachments and screenshots it's a great new set of technologies for use in your tests.

So, we started off with what's new in testing and as you can see, quite a lot.

We have many new APIs for you to use and we didn't even touch on all of them here today.

So, we have lots of new APIs and we have the new workflow and continuous integration features in Xcode, Xcodebuild and Xcode Server and we have those great performance improvements in UI testing.

So, thanks for coming today.

The session, page session of that link above has all the related resources and documentation, everything you're looking for.

And of course, we have lots of sessions in the past and one tomorrow that I encourage you to check out.

Also, the previous years' sessions have some really great information about other parts of testing in Xcode that we didn't really take a close look at today.

Thanks.

Apple, Inc. AAPL
1 Infinite Loop Cupertino CA 95014 US