Understanding XCUITest screenshots and how to access them Aug 20 2019

Getting the screenshots of your app doesn’t need to be complicated, and it shouldn’t require you to install additional applications to do it. Sure you can use any other third-party library, but sometimes simple is better. Also, it is always fun to explore and understand more of the process so you can be the judge on what works best for your case. In this post, I’ll show you how to work with XCUIScreenshot so you can have easier access to your screenshots and later you can build any scripts to process the screenshots.

First, let’s have a look at some cases where you would want to get screenshots of your app.

Why collect screenshots?

There are some cases where you would want to collect screenshots of your app:

Whatever the case might be, it is useful to know how to take the screenshots and more importantly, how to access them.

I’ll assume you have familiarity with XCUITest. But in case you are not so confident I’ll do a quick review of UI Testing on Xcode, so we have a base to start from. If you would like more resources on UI Testing, check the “Related Topics” sections for more links on the topic.

Using XCUITest to navigate your app

If we want to build tests for our User Interface, we can make use of Apple’s XCUITest framework. The framework provides utilities to launch the application on a simulator, and also emulate the user navigation and gestures (taps, swipes, pinch, etcetera). During the simulated user navigation, we can run assertions on what is being displayed on the screen (i.e. verifying the existence of UI elements).

Let’s see some examples. First, validate that the first screen the user sees when our application launches contain a button with the label “Login”.

1
2
3
4
5
func testMainScreen() {
  let app = XCUIApplication()
  app.launch()
  XCTAssertTrue(app.buttons["Login"].exists)
}

If that assertion is true, the test will pass, if it’s false, it’ll fail. As I mentioned before we can simulate user interactions. Let’s imagine we are going to let the user login as a guest by tapping a button with the label “Login as a guest”. Also, to verify that the user reached the second screen, we are going to confirm that the app displays some static texts that say: “Welcome anonymous”.

1
2
3
4
5
6
func testLoginAsGuest() {
  let app = XCUIApplication()
  app.launch()
  app.buttons["Login as guest"].tap()
  XCTAssertTrue(app.staticTexts["Welcome anonymous"].exists)
}

I think you get the idea. I encourage you to take more time to learn UI Testing, it is useful.

Testing allows you to catch bugs before delivering your app to your users. Also, it helps us avoid regressions errors. For example, when some new change in your code generates a bug on a previous workflow that you already have tests for. If you have good tests, refactoring code becomes a much easier and stress-free process because you can always count on your tests, making sure that everything keeps working as you expect.

In simple terms, tests are important. Now, Back to the topic of this post, screenshots.

At any moment during the simulation, you can take screenshots. The screenshots can be of any element that conforms with the XCUIScreenshotProviding protocol, like XCUIScreen and XCUIElement. Let’s have a look.

Taking screenshots

If we want to take a screenshot of our initial screen, we can do the following:

1
2
3
4
5
6
7
8
func testMainScreenshot() {
  let app = XCUIApplication()
  app.launch()

  let fullScreenshot = XCUIScreen.main.screenshot()

  XCTAssertTrue(app.buttons["Login"].exists)
}

With the above code, we took a screenshot, but we didn’t do anything with it so we can’t access it. To save it to the current test, we need to add it as an attachment.

Let’s do that:

1
2
3
4
5
6
7
8
9
10
11
12
13
func testMainScreenshot() {
 let app = XCUIApplication()
 app.launch()

 let fullScreenshot = XCUIScreen.main.screenshot()
 let screenshot = XCTAttachment(screenshot: fullScreenshot)
 screenshot.lifetime = .keepAlways
 // if we don't set lifetime to .keepAlways, Xcode will delete the image if the test passes.

 add(screenshot)

 XCTAssertTrue(app.buttons["Login"].exists)
}

Perfect! Now we have attached it to the test, but how do we view it?

To view them you can check the “Test Report” interface. You’ll see the test report interface right after you run your tests, or by checking the test report in the Navigator (by pressing Command+9).

Where to locate screenshot on test report

As you can see in the test report interface, there is a small eye icon next to the attached file. If you click that icon, you’ll see a preview of the screenshot.

You can take as many screenshots as you want at any point in the user navigation and attach them to the test. But you might want to access the file directly not through Xcode, how can we do that?

Where are test logs stored?

When you run a test in Xcode, all the logs and attachments are stored in the DerivedData directory. To see the location and to access the test logs in Xcode, you can navigate to Xcode’s Preferences and click the Locations item. In there you’ll see the “Derived Data” location. You could also click the arrow next to the path to open it using Finder.

Where to locate screenshot on test report

You can navigate to the DerivedData directory, and you’ll see all your projects. Inside the project you ran the tests, go to Logs > Test you’ll find all the tests results. The results are in an xcresults package. If you are using Finder, you can right-click and Show Package Contents to view the content. If you are using the terminal, you can just cd into it. Inside the xcresult package, you’ll find an Attachment directory with all your screenshots.

We have located the screenshots. But we want a more straightforward way to access them if we’re going to have a predictable path that we could use on our scripts.

Define a specific path to store the Test logs so we can access them easily

By default Xcode stores Derived Data in the following path:

1
/User/[username]/Library/Developer/Xcode/DerivedData

If we are using Xcode, we can change that path in Settings then click Locations and in the dropdown select Custom for Derived Data.

Derived Data custom path on Xcode

If you are using the terminal to run your tests, you can use the --derivedata-path argument to set the path.

1
$ xcodebuild -scheme Testing -project ./Testing.xcodeproj -derivedDataPath '/Users/derik/Desktop/DerivedDataCLI' -destination 'platform=iOS Simulator,name=iPad Pro (12.9-inch) (3rd generation),OS=12.4' build test

Inside you’ll find Logs/Tests with the following hierarchi:

1
2
3
4
5
./DeriveData/Logs/Test/
├── LogStoreManifest.plist
├── Run-Testing-2019.08.20_11-43-51--0600.xcresult
├── ... as many xcresults as tests you've logged
└── Run-Testing-2019.08.20_11-47-42--0600.xcresult

We can obtain the latest xcresults by sorting the list of files by date:

1
$ ls -lrt ./DerivedData/Logs/Tests

or you can get the latest on the LogStoreManifest.plist parsing the file:

1
$ plutil -extract logs xml1 -o - ./LogStoreManifest.plist | xmllint --xpath 'string(//dict[1]/string[contains(text(),"xcresult")])' -

Once inside the xcresults, we will see a file structure similar to the following:

1
2
3
4
5
6
.
├── 1_Run
├── 2_Test
├── Attachments
├── Info.plist
└── TestSummaries.plist

We are only interested in the Attachments directory, where our screenshots are located.

Now that we know where to find them, you can extract them in any way you find more intuitive. You can just open the containing folder using Finder, with a script like the following or any other way you find more comfortable.

1
2
3
4
5
6
7
8
#!/usr/bin/env bash
if [ -z "${DERIVED_DATA_PATH}" ] ; then
  printf "Please set DERIVED_DATA_PATH environment variable.\nYou could set it on the same command:\n"
  printf "$ DERIVED_DATA_PATH=~/Desktop/DerivedDataCLI/ ${0##*/}"
  exit 1
fi
LATEST_XCRESULTS=`plutil -extract logs xml1 -o - ${DERIVED_DATA_PATH}/Logs/Test/LogStoreManifest.plist | xmllint --xpath 'string(//dict[1]/string[contains(text(),"xcresult")])' -`
open "${DERIVED_DATA_PATH}/Logs/Test/${LATEST_XCRESULTS}/Attachments"

With that, you will be able to script any other workflow for your screenshots. But before we are done, let’s improve how we take screenshots and maybe add more information to our screenshot’s.

Improving the code for taking screenshots

First, let’s reduce code repetition and create a function we can call to take screenshots at any point in the simulation. Lets add the takeScreenshot function.

1
2
3
4
5
6
7
func takeScreenshot() {
  let fullScreenshot = XCUIScreen.main.screenshot()
  let screenshot = XCTAttachment(screenshot: fullScreenshot)

  screenshot.lifetime = .keepAlways
  add(screenshot)
}

Now we can call takeScreenshot() at any point, and we’ll have the screenshot attached to our test. That removes code duplication every time we want to take a screenshot, but as you have seen the names of the screenshots are not that helpful.

Providing more information on our screenshots

Imagine getting a list of images with the following names:

1
2
3
Screenshot of main screen (ID 1)_1_0E5EBEBE-CCBE-41B2-B960-75B2E9A512D5.png
Screenshot of main screen (ID 1)_1_430797A3-2D2D-4569-AFA2-555C9EAEFADC.png
Screenshot of main screen (ID 1)_1_FA73B0ED-15DB-4CE1-BAA9-048628647519.png

It’ll be hard to know what each screenshot represents. Let’s improve that by adding a string to identify our screenshots.

1
2
3
4
5
6
7
func takeScreenshot(name: String) {
  let fullScreenshot = XCUIScreen.main.screenshot()

  let screenshot = XCTAttachment(uniformTypeIdentifier: "public.png", name: "Screenshot-\(name)-\(UIDevice.current.name).png", payload: fullScreenshot.pngRepresentation, userInfo: nil)
  screenshot.lifetime = .keepAlways
  add(screenshot)
}

Now when we call takeScreenshot, we provide the string, and the screenshots will have more descriptive names. I also included UIDevice.current.name so we know which simulator we used.

Let’s see an example of how our tests could look:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
func testScreenshots() {
  let app = XCUIApplication()
  app.launchArguments = ["enable-screenshot-data"]
  app.launch()

  takeScreenshot(name: "Dashboard")

  // Open Notification Center
  let bottomPoint = app.coordinate(withNormalizedOffset: CGVector(dx: 0, dy: 2))
  app.coordinate(withNormalizedOffset: CGVector(dx: 0, dy: 0)).press(forDuration: 0.1, thenDragTo: bottomPoint)
  // Open Today View
  let springboard = XCUIApplication(bundleIdentifier: "com.apple.springboard")
  springboard.scrollViews.firstMatch.swipeRight()

  takeScreenshot(name: "Today extension")
}

That will give us a better name for our screenshots.

Final Thoughts

Screenshots can be a valuable source of information, with what we learned you have a good base to start using them in your projects.

There are other third-party tools to automate screenshot taking on your app. But maybe to begin with you can use the scripts we created here and when you outgrow them you can install additional libraries, but with the confidence that you know what is going on behind the scenes.

I hope you find the information here useful, let me know what you think and if you have any questions, let me know, and I’ll be glad to help.

Related topics/notes of interest:


** There is no comment system yet, but you can send me a message on twitter @rderik or send me an email: derik[at]rderik[dot]com.