Railsアプリのrubyのバージョンを2.7から3.1へ上げる作業を1日で終わらせるために大切なこと

# はじめに

お久しぶりの投稿となったQiita。
最近はRails書きながら、AWS構築したり、python書いたり、typescript書いたりな生活です。
たまにはなんか書いてみようかと筆を取りました。
今回はruby2.7を使っていたRailsアプリをruby3.1へアップグレードしたという事例をもとに
__やっておいてよかったこと__ を紹介していきます。

# 結末は2行で

- rubocopは最初から
- テストカバレッジの目標は100%

# アップグレード前の状態

さてアップグレードの話をする前に、アップグレード前の状態について説明しておきます。

アプリケーションとしては2017年頃に開発が始まったもうすぐ6年生。
ruby2.4から始まり、2.7までは毎年順調にアップグレードされていたアプリです。

## コントローラー数等

`rails stats`の結果より

```
+----------------------+--------+--------+---------+---------+-----+-------+
| Name                 |  Lines |    LOC | Classes | Methods | M/C | LOC/M |
+----------------------+--------+--------+---------+---------+-----+-------+
| Controllers          |   6215 |   4882 |     119 |     763 |   6 |     4 |
| Helpers              |     83 |     60 |       0 |       9 |   0 |     4 |
| Jobs                 |    188 |    135 |       4 |      16 |   4 |     6 |
| Models               |   8891 |   4858 |     109 |     567 |   5 |     6 |
| Libraries            |  15215 |  11799 |     291 |    1534 |   5 |     5 |
+----------------------+--------+--------+---------+---------+-----+-------+
| Total                |  30592 |  21734 |     523 |    2889 |   5 |     5 |
+----------------------+--------+--------+---------+---------+-----+-------+
  Code LOC: 21734     Test LOC: 0     Code to Test Ratio: 1:0.0
```

## テストの状態

```bash
$ COVERAGE=1 bundle exec rspec
Finished in 14 minutes 34 seconds (files took 19.89 seconds to load)
6384 examples, 0 failures
```

simplecovのカバレッジレポートより

```
631 files in total.
14461 relevant lines, 14436 lines covered and 25 lines missed. ( 99.83% )
2802 total branches, 2554 branches covered and 248 branches missed. ( 91.15% )
```

## その他

- rubocopを初期から導入済み
- Dockerを使用
  - 本番/ステージングはECS上で動かしている。
  - ローカルの開発環境もDocker(compose)を利用している。

# いざアップグレードの旅へ

__rubocopとテストさえ書いてあれば__、やることは非常に少ないです。

## ruby3.0へのアップグレード

1. Dockerのベースイメージをruby3.0の最新版へ
2. `.rubocop.yml` の `AllCops.TargetRubyVersion` を `3.0` へ変更する
3. `bundle exec rubocop -A` を実行
4. rubocopの警告を修正
5. テストを実行し、修正

### 修正作業で主にやったこと

キーワード引数を受け取る箇所において、以下の変更を入れました。

`foo(params)` => `foo(**params)`

## ruby3.1へのアップグレード

3.0に上げたときと同じです。

1. Dockerのベースイメージをruby3.1の最新版へ
2. `.rubocop.yml` の `AllCops.TargetRubyVersion` を `3.1` へ変更する
3. `bundle exec rubocop -A` を実行
4. rubocopの警告を修正
5. テストを実行し、修正

`foo(bar: bar)` => `foo(bar:)` という自動訂正が働くため、差分は非常に大きくなりました。

しかし、テストで落ちる箇所はなく平和にあがりました。

## パッケージの更新

全gemのバージョンを最新のものに変更し、テストを通せば完了です。

ここまでなんと1日で完了します。

コードの書き方によってテストが大量に落ち修正が大変な場合は、
一度2.7の状態でwarningを消していくというのもありです。

# アップグレードしていく上で大事なこと

こんな感じで1日で3.1になってしまいました。
このあと2週間ほどステージング環境で寝かせた後につい先日リリースされました。

さて、時間をかけずにrubyのバージョンを上げていくために大事なこととして私は以下の2点を押します。

- rubocopは強めの設定で
- テストカバレッジは100%キープ
  - ※異論は認める

詳しくは以下で説明します。

## rubocopは最初から

アップグレードで大活躍のrubocop様。

絶対に初期から、__デフォルトの設定__ でいれましょう。

※ `Style/*` は適宜変更します(`Style/AsciiComments`など)

`Metrics`系はいじらない！
特に `Metrics/AbcSize` は絶対に大きくしない

今回のプロジェクトで `Metrics`でいじってあるのは以下の2箇所のみでした。

個人的には変更せず使いたいものです。

```yml
Metrics/MethodLength:
  Max: 15
Metrics/ClassLength:
  Max: 120
```

### Metrics/AbcSize

コードの難しさの指標とでもいう感じでしょうか。
数値が高いほど修正の難易度が高くなります。

https://en.wikipedia.org/wiki/ABC_Software_Metric

落ちたテストを修正していく作業の中で、AbcSizeが高いものが多い場合に修正の難易度が上がっていきます。

- AbcSizeの最大値は少なめに
- `rubocop:disable Metrics/AbcSize` こういった記述はしない
  - 記述されたコードのレビューにおいては分割できないかを検討
  - どうしても無理。可読性が悪くなる場合のみ`disable`を使用

## テストカバレッジの目標は100%

今回のプロジェクトではカバレッジ99.82%の状態でruby2.7から3.1へアップグレードしています。

高すぎ！80%とかもっと低くても十分！

という意見もあるかと思いますが、

__simplecovのラインカバレッジを100%です。__

viewファイルやjsonの中身のチェックなどすべてを100%というわけではありません。

カバレッジを100%にするために __テストを書きやすいコード__ を書くことが強制されるため、コードが見やすくなり修正が容易になります。

### テストに時間がかかるから毎回coverage計測なんてしてられない

このプロジェクトもテストに時間がかかっています。

そのため、毎週1回月曜日にcoverage計測のテストを実行し、Slack通知しています。

プロジェクトのメンテナーが数値をチェックし通っていない箇所を修正を依頼するようなフローでも良いでしょう。

個人的には以下のようにして、全体ではなく変更部分のコードカバレッジを計測してからPRを出すようにしています。

```bash
COVERAGE=1 bundle exec rspec spec/foo/bar
```

# リリース後の不具合

さて、話が変わりリリース後の話。
今回のリリースですが、リリース後に1件不具合が発生しました。

該当箇所はキーワード引数の箇所

以下のようなコードになっていました。

```rb
def foo
  params = { baz: 1, qux: 2 }
  # ruby2.7まではコレで動く
  bar(params)
  # 3.0以降はこう
  # bar(**params) or bar(baz: 1, qux: 2}
end

def bar(baz:, qux:)
  # something
end
```

※この挙動についての詳細: https://www.ruby-lang.org/ja/news/2019/12/12/separation-of-positional-and-keyword-arguments-in-ruby-3-0/

この処理はSidekiq経由で実行されるJobの処理

- このメソッド単体のテストは書いてある
- JobにEnqueueするところのテストは書いてある

なかったのは、

__`Enqueue`したものが、`Dequeue` されたときに正しく実行できることのテスト__

## Job関係はエンドツーエンドで

rspecに以下のテストコードを追加してもらい修正する形になりました。

```rb
# Enqueue => Dequeue して実行できることのテスト
perform_enqueued_jobs do
  allow(Foo).to receive(:call) # 実行される処理
  described_class.perform_later(x)
  expect(Foo).to have_received(:call)
end
```

今後はレビュー時にエンドツーエンドのテストがあることを確認するようにしていきたいですね

# (おまけ) simplecovの設定

以下のような設定を書いてあります。

```rb:simplecov_helper.rb
# frozen_string_literal: true

require 'simplecov'

SimpleCov.minimum_coverage 99
SimpleCov.merge_timeout 3600
SimpleCov.formatters = SimpleCov::Formatter::MultiFormatter.new(
  [
    SimpleCov::Formatter::HTMLFormatter
  ]
)
SimpleCov.start 'rails' do
  enable_coverage :branch
  # NOTE: これを追加すると branch coverageになる
  # primary_coverage :branch
  add_filter '/spec/'
  add_filter '/vendor/'
end
```

```rb:rails_helper.rb
# frozen_string_literal: true

require_relative 'simplecov_helper' if ENV['COVERAGE']
require 'spec_helper'
# ... snip
```