Upgrade to Pro — share decks privately, control downloads, hide ads and more …

JUnitテストをCI環境で並列で実行する方法とその速度, スケーラビリティ

JUnitテストをCI環境で並列で実行する方法とその速度, スケーラビリティ

プルリクエストを作ると自動的にCI環境で ./gradlew test が走り出す。しかし✅成功あるいは❌失敗のマークが表示されるまでジリジリと待つその時間が長過ぎる。gradleの"maxParallelForks"にも限界があり、テストが不安定になることもあった。Mockitoを使う、@SpringBootTestアノテーションを避ける、リポジトリ層のテストはmysqlではなくh2をインメモリで使う、そんな正攻法を取ろうにもすでに書いてしまった数十,数百KStepのテストコードを目の前にして途方にくれている。

あきらめたら、そこで試合終了ですよ。

このセッションでは、多数のJUnitテストクラスを複数の環境に分散させつつ実行することによって、最終的に✅か❌が出るまでジリジリと待つ時間を短縮する方法をお話します。テストコードには手をつけず、ビルドツール(gradle)とCI環境の工夫だけで、です。 サンプルのSpringBoot/Web+DBアプリケーションプロジェクト=テストクラス75個うち7個はselenium-javaによるe2eテスト=では、 CI環境での自動テスト処理時間の27%削減に成功しました。もっと大量のテストがあるプロジェクトなら並列度を上げれば処理時間削減の効果は大きくなるでしょう。富豪テストによる勝利の栄光をキミに!

Yu Watanabe

June 04, 2023
Tweet

More Decks by Yu Watanabe

Other Decks in Technology

Transcript

  1. #jjug_ccc_b 自己紹介 7 • 渡辺 祐 • Twitter: @nabedge •

    https://www.linkedin.com/in/nabedge/ • https://speakerdeck.com/nabedge • https://nabedge.mixer2.org/about イライラせず待てる時間 = 5分
  2. #jjug_ccc_b 15 import org.apache.commons.io.FileUtils import org.gradle.api.Project import org.gradle.api.tasks.SourceSetContainer import java.io.File

    fun createList(testClassFqcnListFile: File, subprojects: Set<Project>) { val testClassFqcnList = subprojects .flatMap { pj -> val sourceSetContainer = pj.properties["sourceSets"] as SourceSetContainer val files = sourceSetContainer.getByName("test").allSource.files files.filter { it.absolutePath.endsWith(".kt") } } .filter { file -> val sourceCode = FileUtils.readFileToString(file, "UTF-8") sourceCode.contains("@Test") || sourceCode.contains("@ParameterizedTest") || sourceCode.contains("import io.kotest.core.spec.style.") } .map { file -> file.absolutePath .removeSuffix(".kt") .split("/src/test/kotlin/")[1] .replace("/", ".") } .sorted() FileUtils.writeLines(testClassFqcnListFile, testClassFqcnList) }
  3. #jjug_ccc_b FQCNのリストから自分の担当テストクラスを選ぶ 16 1. com.foo.lib.TestA 2. com.foo.lib.TestB 3. com.foo.web.TestC 4.

    com.foo.web.TestD 5. com.foo.web.TestE shard=1/3 のマシンの担当 shard=2/3 shard=3/3 shard=1/3 shard=2/3
  4. #jjug_ccc_b build.gradle.kts import org.apache.commons.io.FileUtils tasks.withType<Test> { useJUnitPlatform() filter { isFailOnNoMatchingTests

    = false val shard: String = project.properties["shard"] as String val match = "(\\d+)/(\\d+)".toRegex().find(shard) val shardIndex = match.groups[1]!!.value.toInt() val shardCount = match.groups[2]!!.value.toInt() return FileUtils .readLines(testClassFqcnListFile, "UTF-8") // 自分のテスト対象としたい FQCNをピックアップ。トランプ配り方式。 .filterIndexed { index, _ -> (index + 1) % shardCount == shardCount - shardIndex } .forEach { fqcn -> this.includeTestsMatching(fqcn) } } } 17
  5. #jjug_ccc_b Github Actionsのyamlを書く runs-on: ubuntu-latest strategy: matrix: shard: [1/3, 2/3,

    3/3] steps: - name: createTestClassFqcnList run: ./gradlew createTestClassFqcnList - name: Test execute by gradle run: ./gradlew test -Pshard=${{ matrix.shard }} 19
  6. #jjug_ccc_b 30 Kotlin Spring Boot Gradle Flyway Thymeleaf Vue.js webpack

    Gradle plugin for Node MySQL AWS-S3 AWS-SQS LocalStack
  7. #jjug_ccc_b 31 • マルチモジュール構成 ◦ foo-web ◦ foo-lib ◦ …

    • テストクラスが全体で合計80個しかない • うち10個はselenium-javaによるe2eテスト • テスト並列化のサンプルにしては小規模すぎ...
  8. #jjug_ccc_b テスト実行の最低限の流れ 32 1. ソースコードをcheckout 2. docker compose up -d

    3. flywayMigrate + α 4. ./gradlew test ※ seleniumによるe2eテストもろとも実行される CI環境ではこのへんで ./gradlew createTestClassFqcnList
  9. #jjug_ccc_b 原因その2 Springの起動処理は重い 35 10sec 1 1 1 Spring Start

    TestA,B,C 13 sec. 10sec 1 TestA 10sec 1 TestB 10sec 1 TestC 11 sec. Serial Test 3-Parallel Test ※Springのテストコンテキストは 複数のテストクラス間で 可能な限り使い回される
  10. #jjug_ccc_b 自分の手元のMacBookが最速な理由=キャッシュ % time docker compose up -d docker compose

    up -d 0.07s user 0.04s system 14% cpu 0.762 total % time ./gradlew :db-migration:run # flywayMigrateしている ./gradlew :db-migration:run 1.33s user 0.13s system 8% cpu 17.531 total % time ./gradlew test ./gradlew test 1.83s user 0.23s system 1% cpu 2:38.16 total 38
  11. #jjug_ccc_b Github Actionsのcache保存機能 39 • yaml上の設定がめんどくさすぎる • キャッシュの退避&warm upの時間 •

    少しでもcache keyが違うと 全てが無かったことになる • 正直アテにしていない(※個人の感想)
  12. #jjug_ccc_b 43 オンプレミス Self Hosted Runner mini-PC x 3 密輸品

    ちゃぶ台 スイッチングハブ ニトリのカゴ
  13. #jjug_ccc_b PC02, 03... 44 Ubuntu GHActions Runner on Ubuntu Oracle

    Virutal Box GHActions Runner on Ubuntu GHActions Runner on Ubuntu PC01
  14. #jjug_ccc_b PC02, PC03… ホストマシンのプロビジョニング 47 MacBook上でansible-playbook PC01 1. apt-get install

    curl jq virtualbox vagrant 2. Vagrantfileの設置 3. OS起動時のsystemdサービスでvagrant up (後述)
  15. #jjug_ccc_b Vagrantfile 48 CLUSTER = { "pc01" => { "self-runner01"

    => { :cpus => 2, :mem => 8192 }, "self-runner02" => { :cpus => 2, :mem => 8192 }, "self-runner03" => { :cpus => 2, :mem => 8192 } }, "pc02" => { "self-runner04" => { :cpus => 2, :mem => 8192 }, "self-runner05" => { :cpus => 2, :mem => 8192 }, "self-runner06" => { :cpus => 2, :mem => 8192 } }, …(snip)... } Vagrant.configure(VAGRANTFILE_API_VERSION) do |config| config.vm.box = "ubuntu/jammy64" …….. 8コア32GメモリのPCの中で 3つの仮想環境に 2コア8Gずつわりあてる
  16. #jjug_ccc_b Vagrantfile 49 CLUSTER[ENV['VG_HOST_NAME']].each do |runner_name, info| config.vm.define runner_name do

    |cfg| cfg.vm.provider :virtualbox do |vb, override| override.vm.hostname = runner_name vb.name = runner_name vb.cpus = info[:cpus] vb.memory = info[:mem] vb.gui = false end # end provider cfg.vm.provision :root_user, type:"shell", path: "scripts/provision.sh", env: { 前ページで作った CLUSTERという 連想配列をループして 複数の仮想環境を構築
  17. #jjug_ccc_b script/provision.sh 1. apt-get install \ git corretto11 corretto17 google-chrome

    docker-ce etc…… 2. jenvをインストール、JDKを登録 3. self-hosted-runner.tar.gz を ダウンロード、展開、起動 50
  18. #jjug_ccc_b SHRマシン群のメンテナンス • テストが増えたのでマシンも増やそう • docker system prune しなきゃ •

    selenium-java用のGoogle Chromeのversion up • githubのfine grained tokenがexpire • ブレーカー飛んだ or 点検で停電 54
  19. #jjug_ccc_b 完成したSHR群を使ってテスト並列化のリベンジ 56 runs-on: ubuntu-latest self-hosted strategy: matrix: shard: [1/3,

    2/3, 3/3……] steps: - name: createTestClassFqcnList run: ./gradlew createTestClassFqcnList - name: Test execute by gradle run: ./gradlew test -Pshard=${{ matrix.shard }}
  20. #jjug_ccc_b • CI待ち時間 5分台 を達成 🎉 ◦ 並列なし→8並列で35%Down • ランニングコストは電気代だけ

    • テストが増えたらマシンを増やして shardの設定値を変えるだけ。 61
  21. #jjug_ccc_b 63 (再)今日 話したいコト 1. CI環境で、 2. JUnitのテストを複数のマシンで 自動的に手分けして実行することで、 3.

    CI待ちの時間を削減する、 4. 2023年5月現在のポピュラーな技術による、 5. 現実的かつシンプルな方法。
  22. #jjug_ccc_b 1. groovy or kotlin プログラミング on build.gradle(.kts) 2. 65

    # Github Actionsのyaml定義 strategy: matrix: shard: [1/3, 2/3, 3/3] steps: - name: createTestClassFqcnList run: ./gradlew createTestClassFqcnList - name: Test execute by gradle run: ./gradlew test -Pshard=${{ matrix.shard }}
  23. #jjug_ccc_b SHRはお金次第 • githubのnormal runner(ubuntu-latest)のコスパ ◦ 0.008USD/minute ◦ CPU =

    2 core, Memory = 7 GByte ▪ https://docs.github.com/ja/actions/using-github-hosted-runners/about-github-hosted-runners#supported-runners-and-hardware-resources • aws-ec2でやるなら t3.large に相当 • オンプレミスなら初期費用+電気代のみ 70
  24. #jjug_ccc_b SHRの構築/運用の自動化は必須 1. スケールアウト=数の勝負=手作業では無理 2. オンプレミスの場合 OracleVirtualBox, ansible, vagrant, shell

    script で十分な自動化が可能 ◦ 電源ボタンだけで再構築できるようにする ◦ もっと数が多い場合にはホストOSの PXEブート&autoinstall / kickstartを 検討すべき 71
  25. #jjug_ccc_b Cost performance 72 • エンジニアの人数, 時給 • 1日あたりPRの数 •

    PRに追加されるコミット数= テストの実行回数 • ✅or❌を待つイライラ時間 • そのプロダクト全体のQCD • Runnerの 必要数 • Runnerの 構築,運用コスト vs
  26. #jjug_ccc_b このセッションで使ったサンプルは小規模すぎる 73 test, test… prepare for test test, test,

    test, test… prepare for test Small PJ Large PJ 複数マシンで並列化 できるのはここだけ