🔑

Swift Package下のTestからはKeychainにアクセスできない

2024/07/29に公開

はじめに

Swift Package 下の Swift ファイルにて、Keychain にアクセスする諸々の処理を書き、その単体テスト (Unit Testing) を書いてロジックの担保を行い、テストを実行すると、下記のようなエラーに遭遇します。

let osStatus = SecItemAdd(query, nil)
print(osStatus) // -34018

OSStatus の詳細を調べることができる OSStatus.com にて詳しくみてみると、どうやら errSecMissingEntitlement というエラードメインで、 Internal error when a required entitlement isn't present. ということらしいです。

もちろん、プロジェクト側のメインターゲットから Run して動作を確認すると、問題なく動作します。
さて、どこに問題があるのでしょうか?

Keychain は App を Host しないとアクセスができない

答えは非常にシンプルです。
Swift Package 内の Test Target は、iOS プロジェクトのツールチェーンに依存がないからです。

Xcode Project 側の Test Bundle を実行するとよくわかりますが、デフォルトでは @main で定義した App 自体も Host されます。( プロジェクト設定 > General > Testing > Host Application )
Xcode Project 側の Test Target は、Swift Package の Test Target と比較すると、 Main Target を Host できる設定項目を保有し、実質のところインテグレーション的なテストを実行できるという大きな違いがあるのです。

Xcode 15.4 時点の現在においては、Swift Package 側の Test Target に対して、Project 側の Main Target を Host するような設定 (TEST_HOST) はないようですので、こうした Entitlements などの App 側の設定ファイルやツールに依存するようなインテグレーション的なテストは書けないよということです。

Workaround

では、現時点で取れる暫定的な対応はあるのでしょうか?

それは、Xcode Project 側に Test Bundle を追加して Host Application に Main Target を選択し、その中で Swift Package 側の @testable import を行なって、Keychain 諸々のロジックのテストを書くということです。
Swift Package にファイルを分けたプロジェクト構成だから、プロジェクト側のせっかく抹殺できた Unit Test Bundle をまた復活させるのか・・・と思うかもしれませんが、こうした App とのインテグレーション的なテストのみをこちらに置くという使い分けをする分にはアリなのではと思います。(自動テストがない状態よりはマシということですね 🙂)
その際、Test Bundle 名を ~~~IntegrationTests~~~AppHostingTests のようにしておけば、無闇にこちらにテストを追加されることも防げるかもしれないのでよいかもしれません。
また、xctestplan にて複数の Test Bundle を束ねたテストの実行をする設定が可能なので、Swift Package 下の Test Target と組み合わせて Scheme の Tests 実行を Test Plan に設定することで ⌘ + U でのテストにおいて全て実行することも可能です。

合わせて、Keychain を隠蔽したクラスは Protocol に準拠させて依存関係を逆転させておき、Swift Package 側の他の Tests においては、Mock を DI させることで参照先のファイルのカバレッジをするのがよいでしょう。

さいごに

Swift Package の Test Target でも Entitlements ファイルを指定できたり、 Hosting Application を設定できるようになって欲しいですね!

Appendix

Discussion