Testing UIKit accessibility in unit tests

I’m writing a custom progress view in UIKit:

A ring-shaped circular progress view
My circular progress view

One of the design goals for the view is to be a drop-in replacement for UIProgressView. This means it should model UIProgressView’s API and behavior as closely as possible. One important aspect of this is accessibility: ideally, my progress view should be indistinguishable from a system progress view to VoiceOver users.

Accessibility properties in unit tests

To this end, I’d like to write a unit test that fails when it finds a discrepancy between my view’s accessibility settings and those of a standard UIProgressView. Before we write that test, though, we should check what UIProgressView’s accessibility properties actually are to make sure we see the correct data:

/// Simple tests for the accessibility properties of UIKit controls,
/// to prove that the test setup is working.
class UIAccessibilityTests: XCTestCase {
  func testUIProgressViewAccessibility() {
    let progressView = UIProgressView()
    progressView.progress = 0.4

    XCTAssertTrue(progressView.isAccessibilityElement)
    XCTAssertEqual(progressView.accessibilityLabel, "Progress")
    XCTAssertEqual(progressView.accessibilityTraits, [.updatesFrequently])
    XCTAssertEqual(progressView.accessibilityValue, "40%")
    // More assertions ...
  }
}

Depending on the environment in which this test is run, some or all of these assertions unexpectedly fail:

  • In a unit test target for a framework, all assertions fail.
  • In a unit test target for an iOS application, only the first assertion (isAccessibilityElement) fails, the others pass.

(And yes, I know some of the assertions are locale-dependent. If your test app has localizations for other languages than English, you may have to configure the correct language in your build scheme when running the test.)

Dominik Hauser found out that we can make the first assertion pass by adding the view to a visible window and giving it a non-zero frame:

  func testUIProgressViewAccessibility() {
    let progressView = UIProgressView(frame: (CGRect(x: 0, y: 0, width: 200, height: 20)))
    progressView.progress = 0.4

    let window = UIWindow(frame: CGRect(x: 0, y: 0, width: 400, height: 400))
    window.makeKeyAndVisible()
    window.addSubview(progressView)

    XCTAssertTrue(progressView.isAccessibilityElement)
    XCTAssertEqual(progressView.accessibilityLabel, "Progress")
    /// ...
  }

Thanks a lot for the help, Dominik (and everyone else who replied to my question)!

Requirements

To summarize, the requirements for testing UIKit’s accessibility properties in a unit test seem to be:

  1. Your test target must be attached to an app target. The tests don’t work in a unit test target for a library/framework, presumably because making a window visible has no effect when the tests aren’t injected into a running app.

  2. Set a non-zero frame on the view under test.

  3. Create a UIWindow (preferably also with a non-zero frame, although this wasn’t relevant in my testing) and add the view to the window. Call window.makeKeyAndVisible.

I say “seem to be” because I’m not certain the behavior is 100% deterministic. Some views, like UILabel, don’t require the additional setup.

More importantly, I have seen my tests fail at least once. When that happened, properties like accessibilityLabel didn’t even return their correct values when logged to the console from the test app, not just in the tests. It was as if the entire accessibility system on the simulator was not running. Attaching the Accessibility Inspector to the simulator once seemed to fix the issue (maybe this caused the accessibility system to restart), and I haven’t been able to reproduce the problem since, even after multiple reboots. Everything appears to be working now.

My test environment: Xcode 11.4.1 with the iOS 13 simulator.