💎

少ない修正×全網羅テスト・目検でRubyのFW(Hanami1->2)更新をトラブルゼロで行った話と保守性など

2023/05/28に公開

はじめに

この記事は、他の方が作ってくれたアップデートの手順書をもとに、Hanami1 & Ruby2からHanami2 & Ruby3へとアップデートする際に意識したことを書いたものです。
はじめに言っておくと、この記事に"ruby", "hanami"といったタグをつけているものの、筆者はJava系エンジニアであって、RubyやHanamiについてはほとんど知識がありませんので、移行時の変更内容の是非については分かりませんが、トラブルゼロで移行するために意識したことを書いていきます。

移行時にやった主なこと※一部

Hanami2移行のために、必要な作業を前もって調査・修正をしてくれた人が居たので、その人の作業を受け継ぐ形で、移行を進めました。なので、Hanami2移行のすべてを書いたわけではないです。
(完璧な記事になってないのに、Zennにあげていいんだろうか?という気がしなくも無いが、移行時に意識したときのマインドが伝われば、まあいいか)。

namespaceの変更

namespaceをディレクトリと一致させる必要があったので、変更した。Zeitwerkへ移行される都合らしい。
Rubyのnamespaceとやらを100ファイルぐらい変えないといけなくて、moduleを追加するとインデントが変わってしまい、他の修正(他の修正も結構なファイル数の修正が必要)とのコンフリクトが多く出てしまい、誤った修正を入れても分からないので、インデントを維持したままmoduleを付与しました。

+ module Aaaa
+ module Bbbb
class HogehogeRepository < ROM::Repository[:hogehoge]
  # 各種メソッド
end
+ end
+ end

Rubyをやっている人が見たら、怒られるのかもしれないが(実際びっくりされたが)、Rubyに対して執着は無いので、こういうことも躊躇は無いです。
むしろ、Java、Kotlin、Scalaなら、ファイルの先頭に書いたpackage宣言の1行だけの差分となるはずなので、namespace追加削除でインデントが変わるほうが奇異に感じるとも言える。
あと、これだと必要なendの個数が分からなくなってしまうが、そこはIDE任せということで。

namespaceを変更した場合、そのファイルを使用している箇所も変更が必要だが、これは正規表現を駆使して、一括置換できた。
組んだ正規表現はRepositoryの場合は、以下のようなもので、HogehogeRepositoryをAaaa::Bbbb::HogehogeRepositoryへ置換します。がclass HogehogeRepositoryは置換の対象外とするという正規表現です。

Search

(?<=\s)(?<!class )(?<!Aaaa::Bbbb::)(\w+)Repository

Replace

Aaaa::Bbbb::$1Repository

これは、移行前のHanami1の段階でも、対応できるので、移行前にやっておくと、Hanami2移行時の差分が少なくなります。移行前に全部終わらせておきました。

controllerからactionへの書き換え、mailerのライブラリの書き方の変更

Hanami2移行時にビジネスロジックで書き換わる唯一の場所なので、結構気を使いました。
メソッドシグネチャやnamespaceが変わるので、また正規表現を駆使して一括置換した(簡単に書けるのと置換の種類が多いので、ここには掲載しないですが)。
これは、他の方が作ってくれたアップデートの手順書の通りに「置き換えルール」の正規表現を組んで、一括置換した。
が、完璧な置換ではないため、他の人にも「置き換えルール」通りに置き換わっているか?「置き換えルール以外の修正が紛れ込んでいないか?」ということを確認してもらった。

Ruby3に対応したraiseの書き方

以下の書き方は、Ruby2では動いていたが、Ruby3では、wrong number of arguments (given 1 expected 0) (ArgumentError)というエラーになるらしい。というか、こんな書き方できたんだ…。

class MyError < StandardError
  def initialize(message:)
    super(message: message)
  end
end

raise MyError, message: 'なんかエラーです'

そこで、raiseの書き方を全部洗い出して、以下ならRuby3でも動くと判定して、目検でチェックした(他のパターンでも動くケースあるんだろうか)。

raise "文字列"
raise エラーオブジェクト
raise エラークラス
raise エラークラス, '文字列'
raise エラークラス, エラーオブジェクト
raise エラークラス.new
raise エラークラス.new(message: '文字列")

Ruby3への移行に伴うkeyword引数対応

https://qiita.com/fursich/items/692f50cc16a3a8d167e7#deprecation-warningを表示する方法

こちらを見ながらlogを見て、keyword引数に対応していない箇所を探し、修正した。

テストや確認

落ちるrspec

delete編

落ちないと思っていたRspecが落ちた。

subject { delete "/hogehoge/#{hogehoge_id}", request_headers }

と間違って書いていた箇所が415 Unsupported Media Typeを返すようになってしまった。
Hanami2では、request bodyに値が入ったdeleteは415エラーを返すようだ。
ミドルウェアをデバッグして、request_headersに想定した値が入っていないことに気づき、以下修正した。

subject { delete "/hogehoge/#{hogehoge_id}", nil, request_headers }

これは結構ハマってしまいました。
IDEのdeleteメソッドのコードジャンプもたくさん候補が出てきて、もうどれなのか辿れない状態。
メソッドシグネチャがおかしいんだろう、とりあえず、Rubyという言語は実行してみて初めてメソッドシグネチャが分かる言語なので、デバッガーでメソッドの中に入れば分かるということで入ってもなんだかメタプログラミングされているせいか、なにがなんだか分からなかったです。
ネット検索したりして、引数の位置が違いそうということで直したら動きました。

controllerで例外をraiseした場合のテスト

subject { post "/hogehoge/", request_params, request_headers }
example do
  expect { subject }.to raise_error(Hogehoge::Exceptions::NotFoundError)
end

こういうふうに書いていたら、raiseされない…。そもそもrequest specなのに、例外キャッチするテストができていたのは、なんでなのかは知らないが、とりあえず、committeeを使う形で以下書き換えた。

subject { post "/hogehoge/", request_params, request_headers }
example do
  assert_schema_conform(404)
end

ビジネスロジックで変更せざるをえない箇所

controllerをactionへの変更やmailerの修正がそうなんですが、これに関しては、Rubyインタプリタになった気持ちで、ひたすら目検でチェックし、他の人にもチェックしてもらった。

全API網羅のテスト

Chromeの開発者ツールを見ながら、コケずに機能するかの確認を、全API網羅で他の人とも協力しながらやりました。
request specなど自動テストもありましたが、カバレッジもそんなに高い状態ではなかったので、人力テストで開発メンバーの協力のもと、実行しました。

外部ライブラリのrequire

requireをしないとエラーになる箇所があったので、requireを追加した。
全機能テストが通っていれば、これは担保されるんじゃないかと思いそうだが、特定のケースでのみ外部ライブラリを使う、というロジックになっている場合に検知できないと思ったので、目検でチェックした。
以下正規表現で、外部ライブラリを使っているところを洗い出した。特定のケースでのみincludeされるとかってのはまあ無いだろう…と見たチェック方法です。

(?<=\s)(?<!(class|module|include) )(?!Hogehoge)[A-Z][a-z]

※Hogehoge始まり以外は、外部ライブラリかもしれないという前提での正規表現です

Hanami.envまたはHANAMI_ENVで分岐している箇所の書き方に誤りがないかのチェック

このチェックをしないと、productionでしか発生しない不具合が発生するかもしれないため。

移行を終えてみて

Rubyは、動かさないと分からない

今まで、Java、Scala等の静的型付け言語をメインで使ってきたので、「動かしてみないと分からない」というのは正直、心理的にツラミがありました。今までは「理論的には大丈夫」と言えたのが、Rubyになると言えないのですから。
同様の理由で、Spring Frameworkも、JavaのフレームワークのくせにSpringの機能を使うためのアノテーションの中に押し込めた文字列が機能するかどうかは、実行時してみないと動くか分からない要素があるので、このへんは好みが分かれるところだと思います。

フレームワーク依存

Javaのフレームワークの場合、ログは、ほとんどの場合、SLF4J(Simple Logging Facade for Java)に抽象化されている、又は、SLF4Jへログの処理を委譲してくれるライブラリがあるので、フレームワークが変わったからといって、ログのライブラリが変わるといったことはほぼない。
変わるとしても、Javaの場合、ログレベルの設定はクラス毎にできるものがほとんどだが、その設定の見直しを移行後のフレームワークのクラスのログの内容に合わせて、ログレベルを上げたり下げたりするぐらいだ。
一方で、Rubyでは、フレームワークに依存しているものが多い傾向があるようで、今回もログのライブラリもフレームワークに依存していたので、ログのフォーマットまで変わってしまい、ログの出力を見直す必要があった。

Spring Frameworkを上手く扱えていない案件で、Spring Frameworkを使わないようにする(この場合、メモリを消費して重くなるだけなので)作業を行ったことがあるが、使っている機能がDIぐらいしかなかったので、Google Guiceへ置き換えるか、一個一個Injectionするコードを書くかして、1日もかからないで移行を終えたこともある。

Rubyは、型情報が静的には分からない関係で、Javaのように型を抽象化してもあまりメリットが生まれないのかもしれない。Rubyは、保守性を犠牲にして「書くのが楽しい」を売りしているため、そのあたりはRubyを選定するのであれば、覚悟が必要だと思った。

書き方の多様性

一部の書き方ではRuby3では動作しないケースもあった。Rubyは、書き方の多様性があるので、それをどの書き方なら、Ruby3でも動作するかどうかを意識しておく必要があったのは辛いところがあった。

Ruby2からRuby3への移行で意識することは比較的少ない

まつもとゆきひろさん「Ruby3の目指す未来 –The Year of Concurrency–」〜RubyKaigi 2019 1日目 基調講演 | gihyo.jp

現在のRubyは多くのユーザが使っており、一度決めた判断を覆すことが難しいと言います。だからこそ今後は、一つ一つの選択をないがしろにせず、進むスピードが遅くなったとしても賢く選択し前進していく方針であると述べました。

アプリケーションをRuby3にあげるときにやること - Qiita

Python2からPython3のような大幅変更は無く、移行のノウハウもすでにネットの記事にあり、安全に移行できるのは楽だと思った。

工数及び反省

着手からリリースまで、「アップデートの手順書」を参考にした移行の全手動スクリプト実行・レビュー2人日とテスト2人日の合計4日人日程度だろうと思っていたが、予想した工数の倍ぐらいはかかった。
他の方が作ってくれた「アップデートの手順書」で完全にすべてを把握するのは無理なので、このあたりと自分のRuby3とHanami2の理解不足が重なり工数が増えた。
とはいいつつも、「他の方が作ってくれたアップデートの手順書」というのが無ければ、工数見積もりすらすぐにはできず、調査から入ることになっていた。
Rubyやフレームワークについて深い理解があれば、エラーメッセージを見て悩まずともどこを直すべきなのかが前もって分かり、やるべきことが把握できるので、この理解がある方であればあるほど工数は少なくいけると思う。

とくに、自動テストは落ちないだろと思っていたが、落ちてしまい、rspecに予想外の修正を入れないといけなかった(Hanami1での書き方がまずかったのが原因)。
Javaの感覚で居て、ログライブラリも変わらんだろと思っていたら、変わってしまって、ログ出力の見直しもしないといけなかった。

Rubyは、JavaやJava系言語(Kotlin, Scala)等に比べると、保守性が低い(これはもうしょうがない)ということが実感できたので、今まで以上に保守性が低くなりそうなコードは書かないようにしようと思った。特に、全体に関わるような修正は、さらに気を使わないといけないということが学びになった。

タイトル通り、リリース時の差分が少ない状態にすることと、全APIテスト+目検という戦略をとって、これでどうやってバグを混入させられるか?を考えてみて、混入させられないと思ったので、リリース後のトラブルゼロだったのは良いこと。

GitHubで編集を提案
InnoScouter(イノスカウター)Tech Blog

Discussion