この記事では、iOSの自動テスト(iOSシミュレーターを使ったUIテスト)を分割・並列実行することによって、実行時間を短縮する方法について紹介します
はじめに
iOS アプリケーション開発において、品質を継続的に向上させるために、自動テスト・CircleCI などのCI/CDツールの導入は、もはや欠かせないものとなりました。
そして、自動テスト・CI/CDを1回導入するだけでなく、継続的に改善していくことが重要です。
例えば、アプリケーション規模や自動テストの数、開発規模が大きくなっていくと、CI/CD におけるビルド・テストの実行時間は長くなってしまい、結果として開発スピードを低下させてしまいます。
特に 今回紹介する XCUITest(Xcode UITest) などのUIテストでは、実際に iOS 実機や iOS シミュレーターを動かしてテストを実行する必要があるため、実行時間が長くなりがちです。
CircleCI では iOS アプリケーション開発で、実行時間を短縮するためのさまざまな機能が揃っています。
- コンピューティングのカスタマイズ(macOS VM は medium、medium Gen2、large のリソースクラスを用意)
- 依存関係のキャッシュ
- テスト分割と並列実行
今回は CircleCI のテスト分割・並列実行を fastlane と組み合わせて、実行時間を短縮する方法について紹介します。
CircleCI と fastlane を使って UIテストを分割・並列実行する方法(概要)
こちらが、今回紹介する iOS アプリケーションのサンプルコードです。
GitHub - tadashi0713/circleci-demo-ios
複数の UIテスト(XCUITest)が用意されており、実行しているテスト内容は一緒ですが、sleep()
を入れることによって実行時間を変えています。
import XCTest
class CircleCIDemoUITests5: XCTestCase {
func testTapButton5() throws {
// UI tests must launch the application that they test.
let app = XCUIApplication()
app.launch()
// Elements
let text = app.staticTexts["text"]
let button = app.buttons["button"]
XCTAssertEqual(text.label, "Hello, world!")
sleep(50)
button.tap()
XCTAssertEqual(text.label, "Button Tapped!")
}
}
fastlane を使って、直列で全てのテストを実行させるためには run_tests Action を使い下記のように記述します。
desc "Run all UITests"
lane :ui_test_all do
run_tests(
scheme: "CircleCIDemoUITests",
devices: ["iPhone 13 (15.4)"]
)
end
この複数ある UIテスト を分割・並列実行する手順としては、以下になります。
- UIテストを実行するための事前ビルド(build_for_testing)を行う
- 複数のmacOS VM・iOS シミュレーターを起動する
- (実行時間に応じて) テストを分割する
- 分割されたテストを並列実行する
- 実行時間が含まれるテスト結果をアップロードする
UIテストを実行するための事前ビルド(build_for_testing)を行う
XCUITest を含め、iOS アプリケーションでテストを実行する際には、実行前にアプリケーションのビルドが必要になります。
今回は、後ほど複数の macOS VM で並列でテストを実行させるために、ビルドの部分のみを事前に行います。
Xcode8 から、build-for-testing
と test-without-building
によって、ビルドとテスト実行を分離することができます。
fastlane で build-for-testing
を実装する場合には、run_tests
の build_for_testing
パラメーターを有効にします。
desc "Run all UITests"
lane :build_for_ui_test do
run_tests(
scheme: "CircleCIDemoUITests",
devices: ["iPhone 13 (15.4)"],
derived_data_path: "dist",
build_for_testing: true
)
end
derived_data_path
パラメーターによって、成果物のパスを指定することが可能です。
これによって、テスト自体は実行せずに、テストを実行させるための事前ビルドができるようになりました。
CircleCI のジョブ(build_for_ui_test
)は以下になります。
build_for_ui_test:
macos:
xcode: 13.3.1
resource_class: macos.x86.medium.gen2
steps:
- checkout
- ruby/install-deps
- run: bundle exec fastlane build_for_ui_test
- persist_to_workspace:
root: .
paths:
- dist
以下の手順を行っています。
- fastlane のインストール(Ruby Orb を利用)
- fastlane の実行(build_for_testing)
- 事前ビルドの成果物を次のジョブで利用できるようにする(persist_to_workspace)
UIテストを分割・並列実行する
事前ビルドのジョブ(build_for_ui_test
)が完了したら、実際にテストを分割・並列実行するジョブ(ui_test_parallel
)を実行します。
ui_test_parallel:
parallelism: 2
macos:
xcode: 13.3.1
resource_class: macos.x86.medium.gen2
steps:
- checkout
- macos/preboot-simulator:
device: iPhone 13
version: "15.4"
- attach_workspace:
at: .
- ruby/install-deps
まず、並列でテストを実行するために、複数の macOS VM を立ち上げます。
parallelism
を指定することで、並列で実行する macOS VM の数を増減することが可能です。
次に以下の手順を実行しています。
- iOS シミュレーターの事前起動(macOS Orb を利用)
- 事前ビルド成果物(attach_workspace)
- fastlane のインストール(Ruby Orb を利用)
これらの手順が終わった後に、実際にテスト分割・並列実行を行います。
実行する fastlane がこちらです。
desc "Run specific UITests"
lane :ui_test_without_building do |options|
run_tests(
scheme: "CircleCIDemoUITests",
devices: ["iPhone 13 (15.4)"],
only_testing: options[:tests],
derived_data_path: "dist",
test_without_building: true
)
end
こちらも run_tests
を利用していますが、異なる点が2つあります。
1 つ目に test_without_building
パラメーターを有効にしています。
今回は事前ビルド(build-for-testing
)を行っているため、こちらのパラメーターを有効にすることでビルドをせずにテストを実行することができます(test-without-building
)。
2つ目に only_testing
パラメーターを指定しています。
このパラメーターを指定することによって、全てのテストではなく特定のテストのみ実行されるようにします。
only_testing
の対象(特定のテスト)は外から渡せるようにします。
CircleCI の設定ファイルでテストを分割・並列実行している部分はこちらです。
- run:
name: Split tests and run UITests
command: |
CLASSNAMES=$(circleci tests glob "CircleCIDemoUITests/*.swift" \
| sed 's@/@.@g' \
| sed 's/.swift//' \
| circleci tests split --split-by=timings --timings-type=classname)
FASTLANE_ARGS=$(echo $CLASSNAMES | sed -e 's/\./\//g' -e 's/ /,/g')
bundle exec fastlane ui_test_without_building tests:$FASTLANE_ARGS
- store_test_results:
path: fastlane/test_output/report.junit
CircleCI CLI である、circleci tests glob
によって対象となるテストファイルを取得し、circleci tests split
によって分割しています。
circleci tests split
には --split-by=timings
フラグを付けています。
後の store_test_results
で JUnit 形式のテストレポートをアップロードしているのが確認できると思います。
--split-by=timings
フラグを有効にすることによって、テストレポートに含まれるタイミングデータを利用して均等にテストを分割しようとします。
これによって、より全体のテスト実行時間を短縮させることが可能です。
実際の並列化されたテストの実行時間については、CircleCI の UI (TIMING タブ)から見ていただくことが可能です。
実行時間の異なる5つのテストを2並列で実行していますが、テスト実行時間にばらつきが少ないことが確認できると思います。
おわりに
この記事では、iOSの自動テスト(iOSシミュレーターを使ったUIテスト)を分割・並列実行することによって、実行時間を短縮する方法について紹介しました
今回紹介したソリューションには以下の特徴があります。
- 各テストの実行時間に応じてテストを分割・並列実行することができるため、より短時間でテスト実行を終了させることができる
parallelism
を増やすだけで、CircleCI のクラウド上で簡単に並列数をスケールさせることができる
CircleCI では、並列数による課金ではなく、1分毎の macOS VM の使用量(クレジット)によって課金されます。
今回のような複数の macOS VM を並列で実行した場合でも、コストパフォーマンスが高い形でご利用していただくことが可能です。
今後より iOS アプリケーション開発のパフォーマンスを上げたい方は、是非参考にしていただければと思います。