Tabelog Tech Blog

食べログの開発者による技術ブログです

Spek やめました

この記事は 食べログアドベントカレンダー2023 の2日目の記事です。

こんにちは、食べログAndroidアプリをメインで担当しているsadaです。

今回は弊社でAndroidアプリのテストフレームワーク Spek Framework の利用をやめたお話をさせていただきます。

(タイトルが今年のDroidKaigiにあったセッションと似た感じになってますが、ちょっとノリで寄せてしまいました、すみません。)

目次

Spek Framework とは

Spekロゴ

Spek Framework(以下、Spek)とは公式ドキュメント にも書かれている通り、 JavaScriptJasmineRubyRSpec にインスパイアされたKotlinのテストフレームワークです。
所謂、振る舞い駆動開発(Behavior Driven Development、以下BDD)のテストフレームワークであり、テストケースの説明を書くために以下のようなDSLを提供しています。

Androidの公式ドキュメントではJUnit4でのユニットテストの作成方法が紹介されていますが、JUnit4ではテストメソッドでテストケースの説明を書くため、以下のような分かりづらい点があります。

  • テストケースの階層化ができないため、命名が冗長になってしまう
  • テストクラス内の実行順序が保証されてないため、一覧で見た時にわかりにくい

それらを改善できるだけでなく、各テストケースがどういったものかわかりやすくなるのがメリットになるかと思います。

Spek Framework を利用していた理由

弊社では以下の理由でSpekを利用していました。

  • 上述したJUnit4で感じるデメリットを解消したかった
  • 食べログのサーバーサイドはRuby on RailsでRSpecを利用しているため、少しでも馴染みのあるものを採用したかった
  • 2019年4月に導入しましたが、当時はまだユニットテスト自体を書く文化がなかったため、書きやすくかつ興味をもってもらえるようなものを選定して、モチベーションを少しでも上げたかった

一番最後の理由「興味をもってもらえるようなものを選定」に関しては、こんな理由でいいの?と思われる部分かもしれませんが、今までやっていなかったことを新しく始めるにはモチベーションをどれだけ高くできるかはすごく重要なので、選定理由にするには十分価値はあったと考えています。
(もちろん他のメリットがあるのは前提ではありますが)

そして当時Spekについては、DroidKaigiなどのカンファレンスでもいくつかのセッションで紹介されていて、注目度も高かったため、そういった役割としては十分効果があったのではないかと思います。

導入からどの程度テストコードが増えていったかは以前書いたブログ「食べログAndroidアプリの技術的負債解消への道のり」の「開発環境の最新化やテストフレームワーク導入」のセクションで触れていますので、良かったらご覧ください。

Spek Framework をやめた理由

さて、ここからが本題です。
ここまで色々検討してSpekを採用していたわけですが、今年Spekの利用をやめるという判断に至りました。
その理由は以下の通りです。

  • Spekの開発が止まっている
  • Spekで書いたことがない人にはやや学習コストがかかる

やはり、一番大きなきっかけとしてはAndroid Studioのプラグインの最新版がリリースされてないために、Android Studioのアップデート時に困ってしまった点になります。
もちろん、SpekはOSSなので、コントリビュートやForkするなどの選択肢もあったわけですが、今後それをずっと続けていくというのも悩ましいという状況でした。

そこで、改めて検討したところ、SpekをやめてJUnit5を採用することにしました。
簡単に理由をまとめると以下の通りです。

  • JUnit5ならJUnit4で感じるデメリットを解消できそう
    • @Nestedなどのアノテーションをinner classにつけることで、テストケースの階層化ができる
  • 現在所属しているアプリエンジニアだと、RSpec書いてる人は多くなかった(選定当時より減った)
  • JUnit5はAndroidで利用するには3rd-party製のプラグインが必要だがメンテされているし、JUnit5自体は十分メジャーなフレームワーク
  • 後述しますが、JUnit5へ簡単に移行できそうな目処もあった

Spek Framework から JUnit5 への移行

とはいえ、いざテストコード全てを書き直すとなるとかなり大変になります。
移行を決定した段階でテストコードが約12万行程度あったため、すべてを手作業で移行するということは最初から考えていませんでした。

ハードワーク

前述した@Nestedなどのアノテーションを使えば、Spekで記述したテストコードを概ね同じような階層で作れることはわかっていたため、一括で変換するスクリプトを作成し、それを元に手作業で微修正するという方法を取りました。
Spek導入時にはどういったポリシーでテストコードを書いていくのかまとめていたために、書き方がある程度統一されていたこともあり、スクリプトでの変換が可能でした。

ちなみに変換スクリプトは以下のようなものです。

  • メソッド名に使えない文字(./)などは一旦_に置換
  • describecontextなどの部分を@Nested inner classに置換
  • itの部分を@Test funに置換
  • beforeEachafterEachはアノテーションに置換
  • 必要なimportを追加して、不要なimportを削除

ちょうど同じような課題を抱えている方がdetektのissueにスクリプトを投稿されていたので、それを参考にさせていただきました。

実際に使ったスクリプトのは以下のようなものです(一部抜粋)
※Gherkinスタイルで書いてる場合は適宜読み替えてください

replace_all() {
  find $DIRECTORY -name $NAME -type f -print0 | xargs -0 gsed -i -E "$1"
}

replace_all '/ (it|test|context|describe)\(.*\)/s/[.:`>\/]/_/g'
replace_all '/ (it|test|context|describe)\(.*\)/s/\[/"/g'
replace_all '/ (it|test|context|describe)\(.*\)/s/\]/"/g'
replace_all 's/(object|class) (.*) : Spek\(\{/class \2 {/g'
replace_all 's/^\}\)$/}/g'
replace_all 's/describe\("(.*)"\)/@Nested inner class `\1`/g'
replace_all 's/context\("(.*)"\)/@Nested inner class `\1`/g'
replace_all 's/it\("(.*)"\)/@Test fun `\1`()/g'
replace_all 's/beforeEachTest \{/@BeforeEach fun setUp() {/g'
replace_all 's/afterEachTest \{/@AfterEach fun tearDown() {/g'
replace_all 's/beforeEach \{/@BeforeEach fun setUp() {/g'
replace_all 's/afterEach \{/@AfterEach fun tearDown() {/g'
replace_all '/^package /a \\nimport org.junit.jupiter.api.Nested\nimport org.junit.jupiter.api.Test\nimport org.junit.jupiter.api.BeforeEach\nimport org.junit.jupiter.api.AfterEach'
replace_all '/^import org.spekframework.spek2/d'
replace_all '/^\}\) \{/d'

このようなスクリプトで一括変換後、テストコードのフォルダに対してファイルフォーマットをかければ完成!、、、、、とはなりませんでした(泣)

失敗

変換自体はうまくいったのですが、いざ中身を見てみると以下のような問題が発生しました。

  1. 階層が深くなったり、説明がながいものがあると、ビルド後のclassファイル生成時にエラーになる(ファイル名が長すぎるため)
    • この辺はパッケージ構造にもよると思います
  2. 同じ階層に同じ説明があるとコンパイルエラーになる(同じメソッドになってしまうので)
  3. テストの記述が間違っている場合はもちろん変換がうまくいかない
    • これはむしろテストのバグに気づけたので良かったです

特に、弊社ではテストケースの粒度や階層がかなり細かくなっていたため、1.の問題が発生しやすく、階層に関してのリファクタリングをかなり実施しました。
とはいえ、すべてを手作業で修正するよりはかなり早く修正できたので、その程度で済んで良かったとは思っています。

まとめ

個人的にはSpekの書き味は割と好きだったのですが、今後の保守性等を考えてJUnit5に移行することにしました。
今回は移行するコストもそこまで掛からないと想定できたため、割とすぐ判断できたのですが、もし工数が相応にかかってしまう場合は、どうするかの判断は悩ましいケースもあるかと思います。

そのためにも導入時の選定などももちろん大事ですが、導入後もしっかりとポリシーを持って運用していくと、いざ移行が必要になった時もある程度ルールに則って移行できるケースも多いと思います。(今回のように)

ソフトウェア開発ではOS・ライブラリなどの更新や流行り廃りなども多く、ある程度の入れ替えや廃止が発生するのは仕方がないと思っています。
ただ、その度に多くの時間もかけてられないのも現状なので、それらをスムーズにできるように、ルールを設けたり、設計を工夫したりすることで、すばやく変化に対応していける状態にしておくというのも大事になります。
今回はその一例として紹介させていただきました。なにかの参考になれば幸いです。

明日は Shimiiii さんの「食べログについて友達11人にインタビューしてみた話」です。お楽しみに!

最後に

食べログAndroidアプリ開発にご興味を持っていただけた方はぜひ気軽にお声がけください! カジュアル面談も大歓迎ですので、ご希望の方はフリーテキストに、「カジュアル面談希望」と記載ください。