How to access app logs from UI Tests on iOS
There is no secret that the UI tests are pretty limited in terms of accessing app internals. That’s why sometimes you have to craft some hackish workarounds to bypass these limitations.
The app logs are one of those things that would be quite useful to have access to from UI tests to observe any events that happen in the app or whatnot.
Even though there is nothing out of the box that could help us, let’s see how we can achieve this after all.
Sample app
As an example, I created a simple app that has an image view and logs a message when it’s tapped:
import SwiftUI
struct ContentView: View {
var body: some View {
VStack {
VStack {
Image(systemName: "photo")
.font(.system(size: 150))
.accessibilityIdentifier("imageView")
.onTapGesture {
print("Image was tapped!")
}
}
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color.brown)
}
}
@main
struct SampleApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
That’s how our picturesque sample app looks like:
LogView injection
To access the logs from UI tests, we need to inject a view that’ll display them for us. But don’t worry:
- it will be there only in debug builds
- it will be completely transparent
- so basically, nothing changes in UI, the tree hierarchy is where the magic happens
struct ContentView: View {
@State private var log: String = ""
var body: some View {
VStack {
VStack {
Image(systemName: "photo")
.font(.system(size: 150))
.accessibilityIdentifier("imageView")
.onTapGesture {
let event = "Image was tapped!"
print(event)
log += event
}
#if DEBUG
.overlay {
Text(log)
.accessibilityIdentifier("logView")
.foregroundColor(.clear)
}
#endif
}
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color.brown)
}
}
Sample test
Now, let’s access the logs from the sample UI test and assert that the event was logged:
import XCTest
final class SampleUITests: XCTestCase {
func testExample() throws {
let app = XCUIApplication()
app.launch()
app.images["imageView"].tap()
XCTAssertTrue(app.staticTexts["logView"].label.contains("Image was tapped!"))
}
}
Finally, that’s how the tree hierarchy snapshot looks like:
Element subtree:
→Application, 0x153d0e440, pid: 61338, label: 'SampleApp'
Window (Main), 0x153d10f20, ((0.0, 0.0), (393.0, 852.0))
Other, 0x153d0aa00, ((0.0, 0.0), (393.0, 852.0))
Image, 0x153d07220, ((110.0, 371.0), (173.0, 135.0)), identifier: 'imageView', label: 'Photo'
StaticText, 0x153d07340, ((124.5, 417.3), (144.0, 42.3)), identifier: 'logView', label: 'Image was tapped!'
Afterword
Hope you got the idea. Making the LogView available across all app screens requires some boilerplate code or some smart architecture, so I’ll leave it up to you. Happy hacking! 🤠