目次

目次

SwiftでUIViewにaccessibilityIdentifierを簡単に設定する

アバター画像
後藤新
アバター画像
後藤新
最終更新日2024/06/14 投稿日2024/06/14

1. はじめに

こんにちは、NX開発推進部iOSアプリ開発グループの後藤です。

私たちのチームでは、自動テストにMagicPodを利用しています。 MagicPodは、モバイルアプリやウェブアプリのテストを自動化するためのAIテスト自動化プラットフォームです。 今回はMagicPodについての詳細な説明は割愛します。

当初、MagicPodのテストでxPathロケータを用いてUI要素を検出させていましたが、UI要素は存在しているものの検出できずにテストが失敗する問題が発生していました。調査を進める中で、テストの安定性をより高めるためには、UI要素の検出方法をxPathロケータからaccessibility idロケータに変更することが効果的であることがわかりました。

MagicPodは、内部的にアプリのシステム情報を利用して操作対象のUI要素を特定するため、要素を特定しやすいユニークIDがなかったり、変更されてしまったりすると、テスト失敗の原因となります。ユニークIDを各UI要素に付与し、変更しないようにすることで、安定したテスト運用が可能になります。 MagicPodヘルプセンター:自動テストを簡単にするためのアプリ実装の工夫を知りたい

accessibility idロケータを設定するメリットをまとめると以下のようになります。

  • テストの安定性向上: 要素が一意に識別されるため、テストの際に正確な要素を検出しやすくなります。
  • メンテナンスが容易: MagicPodのテストケースがUIの変更に強くなり、メンテナンスが容易になります。
  • テストの高速化: xPathロケータを使用するのと比べて、テストの実行が速くなります。

accessibility idロケータを利用するためには、アプリ内の各UI要素に対して一意のaccessibilityIdentifierを設定する必要があります。 そこで、この記事ではUIViewにaccessibilityIdentifierを簡単に設定する方法を紹介します。

2. accessibilityIdentifierの設定方法

accessibilityIdentifierを設定する方法は、XcodeのIdentity Inspectorで設定する方法とコードで設定する2種類の方法があります。

Identity Inspectorで設定する

Identity Inspectorで設定する方法では、Xcode右側ペインのShow the Identity InspectorのAccessibilityの項目から設定できます。

IdentityInspector

しかしこの方法では、accessibilityIdentifierを手動で設定する必要があり、管理もしづらいという問題点があります。 また、UITableViewCellやUICollectionViewCellのように、動的に生成される要素に関しては設定することができません。

そこで、これらの問題を解決するためにプロトコルを利用して、コードベースでaccessibilityIdentifierを設定する方法を次に紹介します。

プロトコルを利用してaccessibilityIdentifierを設定する

UIViewのサブクラスでaccessibilityIdentifierを設定するプロトコルとその拡張を作成しました。

import UIKit

protocol AccessibilityIdentifierSettable {
    func setAccessibilityIdentifiers(_ target: UIView?)
}

extension AccessibilityIdentifierSettable where Self: UIView {
    func setAccessibilityIdentifiers(_ target: UIView? = nil) {
        #if DEBUG
        let mirror = Mirror(reflecting: target ?? self)
        // 最も近いViewControllerのクラス名を取得
        let viewControllerClassName = getViewControllerName()

        for child in mirror.children {
            guard
                let view = child.value as? UIView,
                let propertyName = child.label?
                    .replacingOccurrences(of: "$__lazy_storage_$_", with: "")
            else {
                continue
            }

            var accessibilityIdentifierElements: [String] = [propertyName]

            // indexPathを取得して一意になるようにする
            if let collectionViewCell = self as? UICollectionViewCell,
               let collectionView = collectionViewCell.superview as? UICollectionView,
               let indexPath = collectionView.indexPath(for: collectionViewCell) {
                accessibilityIdentifierElements.append(indexPath.description)
            }

            // indexPathを取得して一意になるようにする
            if let tableViewCell = self as? UITableViewCell,
               let tableView = tableViewCell.superview as? UITableView,
               let indexPath = tableView.indexPath(for: tableViewCell) {
                accessibilityIdentifierElements.append(indexPath.description)
            }

            // accessibilityIdentifierにクラス名を付与し重複を防ぐ
            view.accessibilityIdentifier = viewControllerClassName + "-" + accessibilityIdentifierElements.joined()
        }
        #endif
    }
}
コード解説
  • Mirror API: Mirrorオブジェクトのプロパティやメソッドに対する情報を取得できます。Mirrorを使用して、UIViewのプロパティにアクセスし、各プロパティに適切なaccessibilityIdentifierを設定します。
  • accessibilityIdentifierを一意にする工夫: CollectionViewやTableViewのCellに対しては、indexPathをaccessibilityIdentifierに追加して一意になるようにします。他にも、複数の画面で同じUIが使われていてもaccessibilityIdentifierを一意にするために、UIViewを使用しているViewControllerの名前を付与するようにしています。

また、ViewControllerの名前を取得するために以下のメソッドをUIViewのExtensionとして定義しました。

extension UIView {
    func getViewControllerName() -> String {
        var responder: UIResponder? = self
        let defaultControllerName = "UnknownViewController"

        while responder != nil {
            if let viewController = responder as? UIViewController {
                let className = String(describing: type(of: viewController))
                return className.components(separatedBy: ".").last ?? defaultControllerName
            }
            responder = responder?.next
        }

        return defaultControllerName
    }
}

3. 実際の適用方法

以下のサンプルコードは、Cellのクラスを先ほどのプロトコルに準拠させて、accessibilityIdentifierを設定する例です。 accessibilityIdentifierを付与したいViewの layoutSubviewsのタイミングでメソッドを呼ぶことで、Cellの再利用時にaccessibilityIdentifierのindexPathが書き変わってしまう問題を防いでいます。

CellのindexPathが [0, 0]HogeViewControllerで使われていた場合、titleLabelにはHogeViewController-titleLabel[0, 0]imageViewにはHogeViewController-imageView[0, 0]というaccessibilityIdentifierが付与されます。

final class CustomTableViewCell: UITableViewCell, AccessibilityIdentifierSettable {
    private let titleLabel = UILabel()
    private let imageView = UIImageView()

    override func layoutSubviews() {
        super.layoutSubviews()
        setAccessibilityIdentifiers()
    }
}

確認方法

実際にXcodeのView HierarchyでaccessibilityIdentifierが設定されていることを確認できます。 これにより、正しく設定されたかどうかを確認できます。

accessibilityIdentifier

4. accessibilityIdentifierの設定によるテスト実行の効果

accessibilityIdentifierを一意に設定することで、xPathロケータではなく、accessibility idロケータを選択できるようになりました。その結果、指定したUI要素を検出できずにテストが失敗する問題が減り、より安定したテスト運用が可能になりました。

5. まとめ

本記事では、accessibilityIdentifierを設定することのメリットや、プロトコルを使ってaccessibilityIdentifierを設定する方法について紹介しました。

accessibilityIdentifierを一意に設定することで、テストの安定性が向上します。 また、プロトコルを利用することで、accessibilityIdentifierの名前を毎回考える手間が省け、プロパティが増えた場合でもaccessibilityIdentifierが自動的に付与されるため、管理が簡単になります。

この記事がテストの運用に役に立てば幸いです。

アバター画像

後藤新

目次