はじめに
お久しぶりの投稿となった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
テストの状態
$ 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へのアップグレード
- Dockerのベースイメージをruby3.0の最新版へ
-
.rubocop.yml
のAllCops.TargetRubyVersion
を3.0
へ変更する -
bundle exec rubocop -A
を実行 - rubocopの警告を修正
- テストを実行し、修正
修正作業で主にやったこと
キーワード引数を受け取る箇所において、以下の変更を入れました。
foo(params)
=> foo(**params)
ruby3.1へのアップグレード
3.0に上げたときと同じです。
- Dockerのベースイメージをruby3.1の最新版へ
-
.rubocop.yml
のAllCops.TargetRubyVersion
を3.1
へ変更する -
bundle exec rubocop -A
を実行 - rubocopの警告を修正
- テストを実行し、修正
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箇所のみでした。
個人的には変更せず使いたいものです。
Metrics/MethodLength:
Max: 15
Metrics/ClassLength:
Max: 120
Metrics/AbcSize
コードの難しさの指標とでもいう感じでしょうか。
数値が高いほど修正の難易度が高くなります。
落ちたテストを修正していく作業の中で、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を出すようにしています。
COVERAGE=1 bundle exec rspec spec/foo/bar
リリース後の不具合
さて、話が変わりリリース後の話。
今回のリリースですが、リリース後に1件不具合が発生しました。
該当箇所はキーワード引数の箇所
以下のようなコードになっていました。
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に以下のテストコードを追加してもらい修正する形になりました。
# Enqueue => Dequeue して実行できることのテスト
perform_enqueued_jobs do
allow(Foo).to receive(:call) # 実行される処理
described_class.perform_later(x)
expect(Foo).to have_received(:call)
end
今後はレビュー時にエンドツーエンドのテストがあることを確認するようにしていきたいですね
(おまけ) simplecovの設定
以下のような設定を書いてあります。
# 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
# frozen_string_literal: true
require_relative 'simplecov_helper' if ENV['COVERAGE']
require 'spec_helper'
# ... snip