開発中の iOS アプリでアクセシビリティの品質を担保する方法について考えた

本記事で対象とするのは以下のようなアプリです。
- アクセシビリティに関する問題が多くあるアプリ
- 継続的に新機能開発をする必要がある
- アクセシビリティ向上に取り組んでいきたい

このようなアプリでアクセシビリティ向上を進める際、新しいアクセシビリティの問題が出た場合に検知したい、と思い、そのためにユニットテストを使った方法を考えたのでメモとして残します。最後に述べますがこの方法には課題があります。

# 問題意識
アクセシビリティは、誰もがサービスを利用できるという観点でとても重要です。
しかし、QA 体制やアクセシビリティへの配慮が浸透していないと、新しく実装した画面や機能がアクセシビリティに配慮されていなかったり、デグレでアクセシビリティが低下することなどが考えられます。
このような課題をテストなどで担保できないか考えました。

# 既存のテスト方法
Accessibility Inspector というツールで、`Run Audit` を実行すると、アプリにどのようなアクセシビリティの問題があるかを確認することできます。WWDC23 では、この `Run Audit` を XCUITest 内で実行できるようになったと発表されました。（詳しくはこちらの動画をご覧ください：https://developer.apple.com/wwdc23/10035 ）
以下のような UITest の関数を用意すると、アプリを起動してアクセシビリティの問題があるかを確認してくれます。問題がなければテストが成功し、問題があればテストが失敗します。
```swift
func testAccessibility() throws {
    let app = XCUIApplication()
    app.launch()
    
    try app.performAccessibilityAudit()
}
```
しかし、アクセシビリティ向上に取り組んでいる途中では、一気に全ての問題を修正するのは難しそうです。そうなると、このテストが常に落ちる状態となってしまいます。

そのような場合、特定の問題を無視する方法も用意されているため、それを使うと良さそうです。
下記のコードでは、
- `for: [.dynamicType, .contrast]` でダイナミックタイプとコントラストの問題のみについてテストすることを指定している
- クロージャーで、`"My Label"` ラベルのコントラストの問題については無視するように、`true` を返している
```swift
try app.performAccessibilityAudit(for: [.dynamicType, .contrast]) { issue in
    var shouldIgnore = false
          
    // ignore contrast issue on "My Label"
    if let element = issue.element, 
       element.label == "My Label",
       issue.auditType == .contrast {
           shouldIgnore = true
    }
    return shouldIgnore
}
```
しかし、`for:` で問題の種類を限定してしまうと、新機能を開発している時はその問題についてチェックしたい、などのケースを拾えません。無視する問題はいちいち書かないといけないため、問題が多くある既存のアプリに適用するのは大変そうです。

理想としては、
- 既存のアクセシビリティの問題については改善中なのでテストが落ちてほしくない
- 新しい問題が増えた時にはテストが落ちてほしい

だなと思いました。

# 考えた解決案
ここで、`performAccessibilityAudit` のクロージャーから問題の情報が取得できる（上記コードの `issue` にあたる）ことに注目し、スナップショットテストが適用できるのではないかと考えました。スナップショットテストを使う場合は、以下のような手順になります。

- 最初に、今のアクセシビリティの問題を記録する
- 新しい問題が増えた時はテストが落ちる（→ 修正する）
- 既存の問題が減った時はテストが落ちる（→ スナップショットを作り直す）

## 実装
使用するライブラリ：
- [swift-snapshot-testing](https://github.com/pointfreeco/swift-snapshot-testing): 1.16.2（数ヶ月前に試していたため若干バージョンが古いです）

まずは確認用に以下のような View を用意します。
```swift
import SwiftUI

struct ContentView: View {
  var body: some View {
    Text("Hello, world!")
      .foregroundColor(Color(uiColor: UIColor.secondarySystemBackground))
      .padding()
  }
}
```
非常に読みにくい文字色に設定しています。
<img src=https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/747207/217514dc-a64c-7043-0e63-86bacec0ecd3.png width=300>

UITest を追加して、以下のようなコードを書きます。
```swift
import SnapshotTesting
import XCTest

final class AccessibilityTestUITestsLaunchTests: XCTestCase {

  override class var runsForEachTargetApplicationUIConfiguration: Bool {
    true
  }

  override func setUpWithError() throws {
    continueAfterFailure = false
    isRecording = false
  }

  func testAudit() throws {
    let app = XCUIApplication()
    app.launch()

    // XCUIAccessibilityAuditIssue にはテスト時に毎回変わる ID が含まれているため、XCUIAccessibilityAuditIssue を丸ごと snapshot することはできない
    var issues: [(element: String?, description: String)] = []

    try app.performAccessibilityAudit() { issue in
      issues.append((
        issue.element?.description,
        issue.detailedDescription
      ))
      return true
    }

    assertSnapshot(of: issues, as: .dump)
  }
}
```
これで一度テストを実行してスナップショットを作ります。
`ContentView` にもう一つ、コントラストに問題のある Text を追加してテストを実行するとテストが落ちます。
![Screenshot 2024-12-08 at 23.39.55.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/747207/00978d7b-f1ba-51ec-913c-7ef2da9a27e6.png)

追加した Text のコントラストを修正して再度テストを実行すると、今度は成功します。
![Screenshot 2024-12-08 at 23.43.33.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/747207/6c12a55b-f144-cca8-cad6-852429030342.png)

最初に追加した `Text("Hello World")` のコントラストも修正するとテストが落ちます。
![Screenshot 2024-12-08 at 23.45.54.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/747207/e11d5607-b09c-187d-ca6d-f1e9617b2f66.png)

この場合、スナップショットを更新することもできますが、既存の問題が全て解決した状態なので、スナップショットテスト自体をやめて良さそうです。
![Screenshot 2024-12-08 at 23.48.50.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/747207/5b293d69-8e5a-92a0-4395-07c26745e207.png)

# ここまでやって思ったこと

この解決案の問題点は UITest を作らないといけないことです。これは `performAccessibilityAudit` を使う時の問題点でもあるのですが、今回ターゲットにしているアプリ（= 一気にアクセシビリティ向上できないアプリ）では特に、UITest を書くハードルが高いのではないかと思いました。

また、`performAccessibilityAudit()` でテストできたとしても、人手での確認は必要です。Audit では最低限必要なアクセシビリティの問題を見つけてくれますが、それ以上のことはできませんし、Audit では問題なかったが実際に使ってみると微妙な挙動というのは存在します。例えば、要素に説明文があるかどうかはチェックできますが、要素の説明文自体を評価はしてくれません。
そのため、より良い体験を作るには、やはり人手での確認は必要です。これは WWDC の動画でも言及されていたことです。

結局人手で確認する必要があるなら、整備コストの高い UITest を使う今回の方法を取るメリットはそんなになさそうです。

# まとめ
開発中の iOS アプリでアクセシビリティの品質を担保するために、UITest とスナップショットテストを用いた方法を考えました。


UITest で `performAccessibilityAudit()` メソッドを使うと、アクセシビリティに関する問題を検知できますが、無視したい問題について柔軟に対応することができないと感じました。そこで、アクセシビリティの問題をスナップショットとして保存するスナップショットテストを使うことによって、以下の要件を満たすことができると考えました。
- 既存のアクセシビリティの問題については改善中なのでテストが落ちてほしくない
- 新しい問題が増えた時にはテストが落ちてほしい

アイディアとしては面白いと思ったのですが、この方法では UITest を使わなければいけません。UITest を使うことを考えると、アクセシビリティの問題が多くある既存のアプリに適用するのは難しそうだと結論に達しました。