Timee Product Team Blog

タイミー開発者ブログ

Railsアップグレードを楽にする取り組み 〜社内向け管理画面編〜

こちらはTimee Advent Calendar 2023シリーズ1の5日目の記事になります。
昨日は @redshoga による Vercel REST APIを用いたステージング環境反映botについて で明日は @yama_sitter による フロントエンドアプリケーションの認知負荷とテスタビリティに立ち向かう です。

タイミーでバックエンドエンジニアをしている id:euglena1215 です。

タイミーはユーザー向け・企業向け・社内向けの機能を1つの Rails アプリケーション上で動かしています。
10/5に Rails 7.1 がリリースされ、タイミーも11/1に 7.1.1 に上げることができました。現在は Rails 7.1.2 が本番で元気に動いています。
Rails 7.1.1 へのアップグレードは比較的スムーズに行うことができたものの、アップグレードのプロセスにはまだ改善の余地があると感じました。今回はどこに改善の余地があると思ったのか、具体的な改善の取り組みについて紹介したいと思います。

背景・課題

タイミーでは、Rails アップグレードの動作確認は手動での動作確認をせずとも自動テストで動作を担保して問題ないという合意が得られています。これはテストの網羅性が高く(Code Line Coverage が 91%)十分に動作が担保できているだろうという前提があるためです。

しかしこのルールにもいくつかの例外があります。例えば、暗号化アルゴリズムの変更やキャッシュフォーマットの変更、 Cookie の属性変更など普段書くアプリケーションロジックの自動テストでは検出が難しいと考えられるものです。これらの例外は毎アップグレードで確認すべき箇所が異なり自動テストで検出するのは現実的ではないと考えているため、自動テスト以外の方法で動作を担保しています。

一方、他の例外として「社内向け管理画面は手動でのチェックを行う」というものがあります。これは社内向け管理画面はテストコードの網羅性が低く、自動テストを信頼できないというのが理由です。理由としてはもっともだと思いますが、ユーザー向けの機能は手動でのチェックをしなくていいのに、社内向けの機能は手動でのチェックが必要なのはチグハグさを感じました。

今回はこの 社内向け管理画面はテストの網羅性が低く自動テストを信用できないため、Rails アップグレードの手間が増えている ことが課題だと捉えました。

指標その1:Code Line Coverage

テストカバレッジが低いのならテストを書けばいいじゃないということで、社内向け管理画面 (/admin/* )の Code Line Coverage を指標としテストを書き始めました。

Code Line Coverage とは

「Code Line Coverage」は、ソフトウェアテストの際に使用される指標です。その目的は、自動テストによって実行されたソースコードの行数の割合を測定し、どの程度のコードがテストされているかを把握することにあります。計算方法は、テストで実行されたコード行数を全コード行数で割り、それを100倍してパーセンテージで表します。この指標はテストのカバレッジ定量的に評価するために使われます。

タイミーの Rails アプリケーションでは codecov を使って以下のようにテストで実行されたコード、実行されていないコードを可視化しています。

この時点での app/admin/* ディレクトリの Code Line Coverage は 75% でした。

前提として、タイミーでは社内向け管理画面を実装するために ActiveAdmin gem を利用しています。ActiveAdmin gem を使うこと自体には社内でも賛否両論ありますが、大いなるデメリットもあれば大いなるメリットもあるということで使い続けています(この辺りの話に興味があればぜひ話を聞きに来てください。カジュアル面談はこちら)。

問題とは、ActiveAdmin を使って管理画面を実装した場合 Code Line Coverage と体感のテストカバレッジが一致しないことです。 代表例を挙げます。下記は User モデルに対応した管理画面を実装するコードです。 この2行を書くだけでいくつのエンドポイントが定義されるかを確認してみましょう。

# app/admin/users.rb

ActiveAdmin.register User do
end

下記は上記2行の実装によって定義されたエンドポイントの一覧です。2行書くだけで9つものエンドポイントが定義されています。

root@ba8730884fed:/usr/src/app# bundle exec rails routes | grep admin/users
batch_action_admin_users POST       /admin/users/batch_action(.:format)       admin/users#batch_action
             admin_users GET        /admin/users(.:format)                   admin/users#index
                         POST       /admin/users(.:format)                    admin/users#create
          new_admin_user GET        /admin/users/new(.:format)                admin/users#new
         edit_admin_user GET        /admin/users/:id/edit(.:format)           admin/users#edit
              admin_user GET        /admin/users/:id(.:format)                admin/users#show
                         PATCH      /admin/users/:id(.:format)                admin/users#update
                         PUT        /admin/users/:id(.:format)                admin/users#update
                         DELETE     /admin/users/:id(.:format)                admin/users#destroy

また、ActiveAdmin は動的にルーティングを生成するため、/app/admin/* ディレクトリ以下のファイルはRails 起動時に読み込まれ評価されます。そのため、 app/admin/users.rb に対応するテストを1つも書かなくても app/admin/users.rb の Code Line Coverage は 100% になります。

もちろんこれは極端な例で、普段は一覧の要素を変更するなど実際にアクセスしないとカバーできないコードが生まれるため Code Line Coverage が 100% になるケースは少ないです。だとしても対応するテストがないのに 50%以上のファイルがいくつかあったりと全体的に高く出過ぎているように感じました。 ActiveAdmin は高機能な DSL によって数行でいくつもの画面を生成できます。そのため、コード量の機能量が比例しません。よって、ActiveAdmin による管理画面の実装において Code Line Coverage は指標として不適切だろうと判断しました。

指標その2:Endpoint Coverage

タイミーでは一番外側のテストとして system spec は書かず request spec を書いています。エンドポイントに対応したテストが1つ以上あれば一定動作は保証されているだろうと考え、 /admin/* のエンドポイントに対して request spec が何割カバーできているかを指標として改善をしていくことにしました。

具体例

GET  /admin/users     テスト🙆‍♂️
GET  /admin/users/:id テスト🙆‍♂️
POST /admin/users     テスト🙅‍♂️

↓ ↓ ↓ ↓ ↓ ↓

テストされたルート数: 2
テストされていないルート数: 1
全ルート数: 3
カバレッジ: 66.67%

ここでは Endpoint Coverage と呼称することにします。(正式名称あれば教えてください。訂正します。)

なるべくシンプルな方法で Endpoint Coverage を集計することにしました。ステップは以下です。

  1. rails routes 相当の情報を取得
  2. spec/requests/* ファイルを読み込みルーティングに対応する describe 句を抜き出す(describe 句があればテストケースは1つ以上あるだろうと判断)
  3. 1.と2.で得られたデータを組み合わせて、テストされているエンドポイント・テストされていないエンドポイントを分類し、割合を算出する

集計用の Rake タスクは以下になります。興味があれば見てみてください。

エンドポイントごとのテストカバレッジを計測するための rake タスク · GitHub

上記の Rake タスクをタイミーの Rails アプリケーションで実行すると以下の結果が得られました。

root@084b7fc12fd0:/usr/src/app# bundle exec rake endpoint_coverage:collect_for_admin
テストされたルート数: 165
テストされていないルート数: 367
全ルート数: 504
カバレッジ: 32.74%
root@084b7fc12fd0:/usr/src/app# bundle exec rake endpoint_coverage:collect
テストされたルート数: 457
テストされていないルート数: 445
全ルート数: 827
カバレッジ: 55.26%

確かに admin はカバレッジが 32.74% と高くないことが分かります。体感としてもそのくらいです。 次に全体のカバレッジは 55.26% でした。こちらの結果には admin も含まれているため、admin を抜くと 78.9% です。

上記の結果より、admin とそれ以外とでは2倍以上の開きがあることが分かりました。これならテストの網羅性が低いという判断も納得できます。ということで、Endpoint Coverage を指標として用い社内向け管理画面のテストの網羅性を高めていくことにしました。

また、「社内管理画面の手動での動作確認では機能の60%もカバーできていないだろう」ということで Endpoint Coverage は 60% を目標として進めていくことにしました。

改善その1:index actionのテストめっちゃ書く

Endpoint Coverage の集計 Rake タスクの副産物として、テストが書かれていないエンドポイント一覧を入手しました。これを元にテストの拡充を進めていきたいと思います。

まずは、index action に対応する request spec を書いていくことにしました。テストケースとしては 200 を返すことを検証します。
この判断をした理由は以下の通りです。

  • index action は一覧を取得するためのエンドポイントであり、多くの利用者は一覧画面を起点にして操作を行うため比較的重要度が高い
  • index action の 200 を返すテストはある程度機械的に追加できるため、テストを追加するのが楽
  • spec ファイルが元々あるのとないのとではテストを追加する際の心理的ハードルが異なるため、とりあえずファイルだけでも作っておくことで他のエンドポイントのテスト追加がされやすくなるのではという期待

というわけでテストを書き始めていると嬉しい誤算に気付きました。それは GitHub Copilot がかなり補完してくれることです。

# frozen_string_literal: true

require 'rails_helper'

RSpec.describe 'Admin::User' do
  describe 'GET /admin/users' do
    # ここまで書くと...
  end
end
# frozen_string_literal: true

require 'rails_helper'

RSpec.describe 'Admin::User' do
  describe 'GET /admin/users' do
    # 以下を全て補完してくれる!
    subject { get admin_users_path }

    context '正常系' do
      before do
        create_list(:user, 2)
      end

      it '200を返す' do
        subject
        expect(response).to have_http_status(:ok)
      end
    end
  end
end

テストが書かれていないエンドポイント一覧は手元にあるため、ほとんどエンドポイントをコピペするだけで index action に対応する request spec を増やしていくことができました。

その結果、以下のように 32.74% → 41.47% までカバレッジを伸ばすことができました 🎉

root@084b7fc12fd0:/usr/src/app# bundle exec rake endpoint_coverage:collect_for_admin
テストされたルート数: 209
テストされていないルート数: 326
全ルート数: 504
カバレッジ: 41.47%

改善その2:使われていない batch_action の削除

テストが書かれていないエンドポイント一覧を眺めていると、多くのリソースに対して batch_action エンドポイントが生えていることが分かりました。これは一覧画面で各リソースにチェックを入れて一括で削除するといった処理を行うために ActiveAdmin が用意しているものになります。
Active Admin | The administration framework for Ruby on Rails

batch_action をデフォルトで有効にするかどうかは設定で変更可能なのですが、タイミーでは有効になっているようでした。

「一括操作ってそんなに行うことある?」と思い過去1年間のログを確認したところ、batch_action が使われているリソースは1つしかありませんでした。そのため、全体では無効化し使われているリソースにのみ batch_action を有効化しました。
これまで定義されていた batch_action は気付かず定義されていたのか、開発者による善意のものだったのかは判断できませんが Code Line Coverage では気付くことが難しく Endpoint Coverage を見ていたからこそ気付けたものかなと思っています。

その結果、以下のように 41.47% → 48.98% までカバレッジを伸ばすことができました 🎉

root@084b7fc12fd0:/usr/src/app# bundle exec rake endpoint_coverage:collect_for_admin
テストされたルート数: 217
テストされていないルート数: 257
全ルート数: 443
カバレッジ: 48.98%

目標としていた Endpoint Coverage 60% にはまだ届いていませんが、元々が 32.74% だったことを考えるとかなり近づいたのではと思っています。

まとめ

Rails アップグレードの手間を減らすために社内向け管理画面のテストを拡充させようと考え、Endpoint Coverage という指標を定義しました。Endpoint Coverage を改善するためにいくつかの改善を行い、約30%から約50%と目標の60%に近づけることができました。Endpoint Coverage を定義し、テストされていないエンドポイント一覧がわかったことで様々な改善アイデアが思いついたように感じます。

これからも Endpoint Coverage を高めていき Rails 7.2 アップグレードでは手動での動作確認が必要なくなるよう頑張っていこうと思います。