3ヶ月で Rails 4.2 → 5.2 にした軌跡


## 概要
[資格スクエア](https://www.shikaku-square.com/)の PM をやっています岩瀬と言います。[資格スクエア](https://www.shikaku-square.com/)は、法律系難関資格に特化したオンライン学習サービスです。

サービスを開始してから数年が経ち、1つの大きな問題がありました。それは Rails のメジャーアップグレードです。

長年の負債が溜まっていたので、なかなかメジャーアップグレードできずに苦しんでいたのですが、フリーランスのカルパスさん（[yhirano55](https://github.com/yhirano55)）の加入で一気に進みました。

カルパスさんが方針を考えて実装し、それをレビューでサポートしたラスタムさん（[rastamhadi](https://github.com/rastamhadi)）はじめ、チーム一丸となって取り組んだ結果、わずか3ヶ月で成し遂げられるミラクルを目撃したので、その内容をまとめました。

## 記事の目的
テストカバレッジが低くても、Rails のメジャーアップグレードができるという知見を共有するためです。

## 背景
Rails メジャーアップグレードは適宜対応していくのが最善ですが、以下の課題があったため、作業が難航していた状態でした。

* サービス成長を優先していたため、Rails のメジャーアップグレードが後回しになっていた
* サービス開始から5年以上が経過していたため、負債が溜まりメンテナンスコストが高くなっていた
* サービス立ち上げ時はテストコードを書いていなかったため、カバレッジが 32 %と低い状態であった

ですが、Rails 6.0 が出るにあたって、いよいよ Rails 4 系のサポートが終了するため、早急に Rails 5 系に上げる必要がありました。

どうしようか悩んでいた時、ラスタムさんの紹介によってカルパスさんが加入して本格的に着手することになりました。

## バージョン
以下は Rails のメジャーアップグレード作業に着手した際に使用していたバージョンです。

| 名称 | バージョン |
| :---: | :---: |
| Rails | 4.2.11.1 |
| Ruby | 2.3.6 |

## 規模
[資格スクエア](https://www.shikaku-square.com/) は、2013年11月にサービスを立ち上げてから5年以上が経過しており、中規模と言えるような状態になっていました。規模の目安として、あくまで参考ですが2020年1月現在での各モジュールの数は以下になります。

| 名称 | 数 |
| :---: | :---: |
| Controllers | 177個 |
| Models | 190個 |
| Code LOC | 108,612行 |

## 戦略
上記の背景と規模を踏まえて、次の戦略で進めることにしました。

1. メジャーアップグレードを優先して最小のテストカバレッジで進める
2. テストでカバーできないものは動作確認で洗い出す
3. 動作確認でも洗い出せなかったものは本番環境に反映後、即対応する

## タイムライン
今回の取り組んだアップグレードのざっくりしたタイムラインは以下になります。

| 年月日 | 内容 |
| :---: | :---: |
| 2019年8月27日 | Rails 5.0 へメジャーアップグレード作業開始 |
| 2019年10月17日 | Rails 5.0.7.2 にアップグレード完了 |
| 2019年10月23日 | Ruby 2.6.5 にアップグレード完了 |
| 2019年11月19日 | Rails 5.2.3 にアップグレード完了 |

## 手順
### Rails 4.2 → 5.0
それではポイントを押さえて、実際に行ったメジャーアップグレード手順を紹介します。

#### 1. 足場の確保
まずは開発環境を整え、テスト実行の足場を確保しました。

* 落ちているテストを直す
* 開発用途の gem を更新する（[lock_diff](https://github.com/vividmuimui/lock_diff) を活用して changelog を追う）

#### 2. モデルとテーブル定義の不一致を解消する
モデルによるレコード生成をテストコードで担保したいのですが、そもそもモデルとテーブル定義が不一致だったので、それを解消しました。

* [annotate](https://github.com/ctran/annotate_models) でモデルとテーブル定義の制約条件（validation）が一致していることを確認しやすいようにする
* 開発環境と本番環境のテーブル定義を一致させる
* テーブルの NOT NULL 制約とモデルの presence validation を一致させる
* テーブルの unique index とモデルの uniqueness validation を一致させる

#### 3. 全モデルに最低限の単体テストを書く
ここで全モデルのテストコードを書きました。単体テストを網羅するのはあまりにも大変なため、最低限レコード生成を担保するテストを整備しました。

* 全モデルの factory を定義する
* レコードの生成が valid となる最低限のテストを用意する

ex. `spec/models/certificate_spec.rb`

```ruby
require 'rails_helper'

RSpec.describe Certificate do
  describe 'factory' do
    it 'has a valid factory' do
      expect(build(:certificate)).to be_valid
      expect(create(:certificate)).to be_persisted
    end
  end
end
```

#### 4. 管理画面に最低限の結合テストを書く
管理画面は gem を使わず、独自実装であったため結合テストで最低限画面表示できることを担保しました。

ex. `spec/features/admin/certificates_spec.rb`

```ruby
require 'rails_helper'

RSpec.feature '管理ツール 資格管理', type: :feature do
  scenario '一覧が閲覧できる' do
    prepare_logged_in_admin_user

    within(:css, '#sidebar') do
      click_link '資格管理'
    end

    expect(page).to have_current_path admin_certificates_path
    expect(page).to have_selector 'h1.page-header', text: '資格管理'
  end
end
```

#### 5. Rails 5.0 の変更点に応じて修正する
[Rails 4.2からRails 5.0へのアップグレード](https://railsguides.jp/upgrading_ruby_on_rails.html#rails-4-2%E3%81%8B%E3%82%89rails-5-0%E3%81%B8%E3%81%AE%E3%82%A2%E3%83%83%E3%83%97%E3%82%B0%E3%83%AC%E3%83%BC%E3%83%89)に記載されている内容を1つ1つ確認しながら対応しました。

* Rails 5.0 から全 Model は ApplicationRecord という抽象クラスを継承する形式のため変更する
* ActionController::Parameters は今後 HashWithIndifferentAccess を継承しないので対応する
* migrationファイルを置き換える

ex. `db/migrate/20150302014237_create_admin_users.rb`

```diff
- class CreateAdminUsers < ActiveRecord::Migration
+ class CreateAdminUsers < ActiveRecord::Migration[4.2]
```


#### 6. Rails 5.0 に上げる Pull Request を作成する
ここで Rails 5.0 に上げる目処が立ったので、 Pull Request を作成しました。

* gem の依存バージョンを解決する
* Rails 5.0.7.2 にアップグレードする
* 設定ファイルの更新（app:update）
* テストが実行できる
* テストを実行し、落ちているものを修正する
* すべての警告を抑止する

#### 7. テスト環境で隅々まで動作確認する
最低限のテストコードでカバーしているため、テスト環境で動作確認を行いました。その動作確認の結果、エラーになった箇所を1つ1つ解決していきました。[資格スクエア](https://www.shikaku-square.com/)で遭遇したエラーは以下になります。

* Rails 5.0 では存在しないコールバックをスキップすると ArgumentError になるので不要なコールバックを削除する
* params をそのまま使うと Rails 5.0 からセキュリティの都合で ArgumentError になるため、hash に変更する
* Rails 5.0 で collection_check_boxes の hidden 要素が最後から最初に変わったため、修正する

#### 8. Rails 5.0 を本番環境にデプロイする
いよいよ Rails 5.0 に変更できる準備が整ったのでデプロイしました。
何か問題が発生しても対応できるようにエンジニアメンバーが稼働している勤務時間帯に反映しました。

#### 9. 本番環境で発生した問題に対処する
実際、デプロイ後にやはり問題が発生しました。具体的には以下のものになります。

* 本番反映前の Rails 4.2 で作られたジョブを Rails 5.0 で動作させるために一時的にパッチを当てる

ex. `config/initializers/temporary_active_record_patch.rb`

```ruby
class ActiveRecord::ConnectionAdapters::AbstractMysqlAdapter
  class MysqlDateTime < ActiveRecord::Type::DateTime
  end
end
```

* Rails アップグレードに伴い、セッションが切れる問題が発生したためにセッションをクリアするコマンドを実行する

```commandline
$ RAILS_ENV=production bundle exec rails r 'Rails.cache.clear'
```

これで Rails 4.2 → 5.0 へメジャーアップグレード完了です。

### Rails 5.0 → 5.2
Rails 5.0 にメジャーアップグレードできましたが、ここで立ち止まらず引き続きマイナーアップグレードを行いました。それは Rails 5.2 まで上げると system spec が使えるようになり、javascript の動作をテストコードで担保できるようになるからです。

#### 1. 整理整頓
メジャーアップグレードで把握した使用 gem の内、不要なものは削除するなど、メンテコストを下げました。

#### 2. Rails 5.1 の変更点に応じて修正する
[Rails 5.0からRails 5.1へのアップグレード](https://railsguides.jp/upgrading_ruby_on_rails.html#rails-5-0%E3%81%8B%E3%82%89rails-5-1%E3%81%B8%E3%81%AE%E3%82%A2%E3%83%83%E3%83%97%E3%82%B0%E3%83%AC%E3%83%BC%E3%83%89)に記載されている内容を1つ1つ確認しながら対応しました。

* ActionController::Parameters における HashWithIndifferentAccess のメソッド呼び出しのサポートが削除されたので対応する
* render :text を render :plain に変更する
* render :nothing を head :ok に変更する

#### 3. ついでに API のテストカバレッジを上げて整理する
Rails で API を担っている領域のテストカバレッジが低かったので、requests spec で担保しました。 

* 正常系と異常系の requests spec を整備する
* 不要なエンドポイントを削除する

#### 4. Rails 5.1 に上げる Pull Request を作成する
ここで Rails 5.1 に上げる目処が立ったので、 Pull Request を作成しました。

* gem の依存バージョンを解決する
* Rails 5.1 にアップグレードする
* 設定ファイルの更新（app:update）
* テストが実行できる
* テストを実行し、落ちているものを修正する
* すべての警告を抑止する

#### 5. Rails 5.2 の変更点に応じて修正する
引き続き[Rails 5.1からRails 5.2へのアップグレード](https://railsguides.jp/upgrading_ruby_on_rails.html#rails-5-1%E3%81%8B%E3%82%89rails-5-2%E3%81%B8%E3%81%AE%E3%82%A2%E3%83%83%E3%83%97%E3%82%B0%E3%83%AC%E3%83%BC%E3%83%89)に記載されている内容を1つ1つ確認しながら対応しました。

* uniq は Rails 5.1 で削除されるので distinct に置換する

#### 6. Rails 5.2 に上げる Pull Request を作成する
ここで Rails 5.2 に上げる目処が立ったので、 Pull Request を作成しました。

* gem の依存バージョンを解決する
* Rails 5.2 にアップグレードする
* 設定ファイルの更新（app:update）
* テストが実行できる
* テストを実行し、落ちているものを修正する
* すべての警告を抑止する

#### 7. テスト環境で隅々まで動作確認する
この段階でテストカバレッジは 47 % と以前よりはだいぶ上がっていますが、
まだまだテストコードで担保できていない機能があるので、テスト環境で動作確認を行いました。

#### 8. Rails 5.2 を本番環境にデプロイする
Rails 5.2 に変更できる準備が整ったのでデプロイしました。

#### 9. 本番環境で発生した問題に対処する
Rails 5.2 のデプロイ後は特にエラーは発生しませんでした。Rails 5.0 の時と同様にセッションが切れる問題は再度発生することが予想されたので対応しました。

これで Rails 5.0 → 5.2 へマイナーアップグレード完了です。

### その後
Rails 6.0 にはすぐに上げることができるのですが、特に使いたい機能があるわけではなかったので、ここで一旦立ち止まり強化することにしました。対応したことは以下になります。

* system spec による結合テストの整備（2020年1月時点でカバレッジ 53 %）
* Dependabot によるバージョン追従
* CI の実行速度の改善と整備
* CD の開発

## まとめ
Rails 4.2 から 5.2 までアップグレードした3ヶ月の軌跡をまとめました。

最初、自分が資格スクエアをメジャーアップグレードしようと考えた時は、正直テストカバレッジが低いので相当難しいなという印象でした。

しかし、効果的な戦略と的確な変更によってわずか3ヶ月で成し遂げられた状況を見て、とても感動しました。そしてシステム面では新しい機能が使用できたり、パフォーマンスが向上したりと様々な恩恵を受けています。

この方法が自分たちと同じようにメジャーアップグレードに悩んでいる方々にとって参考になれば幸いです。