Money Forward Developers Blog

株式会社マネーフォワード公式開発者向けブログです。技術や開発手法、イベント登壇などを発信します。サービスに関するご質問は、各サービス窓口までご連絡ください。

20230215130734

約8年開発されている Rails 製プロダクトを Ruby 3 にバージョンアップするために keyword parameters is deprecated を「網羅的に」検知する方法

こんにちは クラウド経費開発チームクラウド債務支払開発チーム の 宮村(みやむー) @miyamura.koyo です。

最近はチームで Ruby 3 系へのバージョンアップを行なっております。 その際にやるべきことの1つとして、「キーワード引数にハッシュを渡した際のエラーに対処すること」があります。

www.ruby-lang.org

Ruby 3 系にアップデートする際に、上記のエラーが発生するメソッド呼び出しをそのままにしてしまうと ArgumentError が発生してしまいます。 なので、上記のようなエラーが発生するコードを 全て 修正する必要があります。しかし私が今バージョンアップに取り組んでいる Rails app は8年近く開発されており、対象のコードは膨大にあります。しかもこれは gem 内のメソッドも対象だったりするので、gem 側が未対応でエラーになったりすることもあります。 これに対してどのように対処したかを書いていきたいと思います。

単体テストを直す

まずは何はともあれ Ruby3 系にバージョンアップして大量に落ちるテスト(数千以上...!)を直します。 エラーの内容は実際のテストに依存しますが、典型的には以下のようなエラーが発生するかと思います。

ArgumentError:
  wrong number of arguments (given 1, expected 0)

とはいえ対処自体はとても簡単(ハッシュを渡さないようにしたり、 ** つけるようにしたり)なのでひたすら刺身たんぽぽ。

ただ gem 側も Ruby3 の記法に対応したものにアップデートする必要があります。こちらは対象もいくつかあり難航しましたが、それもなんとか乗り切り、テスト全件通ったぞ!となりました。

単体テストでカバーできない場合があった

テストも全件通ったし、 検証環境で確認だ!ということで、意気揚々と QA チームと協力して結合レベルのテストを行いました。 しかしそうするといくつかエラーが発生...。

あれだけテスト通したし、カバレッジも高いはずなのになぜ?と思って調査。 そうすると、主に外部サービスへのリクエストを行うところでエラーになっていることがわかりました。 というのも、こういう外部リクエストを行うメソッドはモックしてテストされているケースが多く、その場合にメソッド呼び出しが適切に行われていなくてもエラーにならない!というケースがあるということがわかりました。

例えば以下のようにキーワード引数を受け取って外部リクエストを行い値を取得する request メソッドを持った HogeClient を考えます。

class HogeClient
  class << self
    def request(params:, options:)
      ..... # 何かしらの HTTP リクエストをして値を取得
    end
  end
end

そしてこれを利用する HogeService の実装とそのテストを考えます。 HogeService#execute!HogeClient.request を内部で使用しており、キーワード引数ではなくハッシュで引数を渡しているため Ruby 3 系ではエラーになります。

class HogeService
  def initialize
    .....
  end

  def execute!
    params =  .....
    options =  .....
    HogeClient.request({ params: params, options: options}) # Ruby 3 系だとエラーになる
  end
end

HogeClient は外部への HTTP リクエスト経由で値を取得するので、テスト中では以下のようにモックされていたのですが、こうしてしまうと、Ruby 3 環境でテストを実行してもエラーが発生しません。

Rspec.describe HogeService do
  describe '.execute!' do
    before do
      allow(HogeClient).to receive(:request).and_return(.....) # 問題のメソッドをモックしてしまうと、実際に呼び出すとエラーになるにも関わらずテストが通過してしまう
    end
  end
end

クラウド経費・債務支払はプロダクトの特性上、外部の色々なサービスと連携することが多いという特徴があります。そのため上記のように単体テストで検知しにくいケースがいくつかありました。 さらに 検証環境では確認できないものなどもあり、これらを単体テストや検証環境での結合テストのみで網羅的に検知するのは非常に難しいことがわかりました。 しかし検証を怠ると ArgumentError が発生してしまうため、何とかしてこれらをリリース前に検知して修正したい!という気持ちがありました。

本番で warning ログを出す

ということで、別のアプローチを模索することにしました。

調べてみると Ruby 2.7.2 になるまでは、上記のようにエラーが出る場合には warning ログが出力されていました。 しかし Ruby 2.7.2 からはこれらは意図的にオプションを有効にしないと出ないようになっています。

www.ruby-lang.org

これを出すには、コマンドラインオプション -w や環境変数 RUBYOPT=-W:deprecated を設定する必要があります。

しかし Rails アプリケーションではどうすればいいでしょう? RUBYOPT=-W:deprecated を設定して起動する方法もあるかと思うのですが、他の起動時の処理でこれらが上書きされたりするケースもあるので、うまくいきませんでした。(require 'bundler/setup' が上書きしたりするっぽい)

ここで Ruby には Warning モジュールという便利なモジュールがあります。 Rails アプリケーション起動時に Warning[:deprecated] = true を記述することでキーワード引数の warning を出すことができます。 これを Rails の起動時に設定することで、Rails アプリケーションでも deprecation warning を出すことができます。

docs.ruby-lang.org

docs.ruby-lang.org

ただ、これだと「標準エラー出力」に出てしまいます。運用している Rails アプリケーションでは標準エラー出力を簡単に可視化できる仕組みになっておらず、調べにくいので、できれば(Sentry, Rollbar のような)監視用のサービスにログを投げたいなと思いました。

ここで再度ドキュメントを読んでみると以下の記述があります。

引数 message を標準エラー出力 $stderr に出力します。 Kernel.#warnの挙動を変更する際は、このメソッドではなくクラスメソッドであるWarning.warnをオーバーライドする必要があります。

(しかもコード例もある...超丁寧!)

なので、Rails アプリケーション起動時に Warning.warn をオーバーライドするコードを書けばいいことがわかります。

以上を踏まえて、以下のようなコードを書いて本番にリリースしました。

# config/boot.rb
if ... # warning を出したい環境かどうかの判定。 prod 環境でのみ出すようにするなど。
  Warning[:deprecated] = true
end
# config/initializers/warning.rb
module Warning
  def self.warn(message)
    if message.include?("keyword parameters is deprecated") || message.include?("the last hash parameter is deprecated")
      # 監視サービスに通知する
    end

    super(message)
  end
end

これで監視サービスに warning 一覧が出るようになりました! 注意点ですが、監視サービスに投げる際に同期的に実行したり実行時間が少しかかるようなものである場合、 warning が発生するリクエストのレスポンスタイムが遅くなるなどの影響が考えられます。監視サービスに通知するライブラリに非同期実行のオプションがあることも多いかと思うので、参考にされる方はこちら気をつけていただけると幸いです。 また、大量のログが監視サービスに出る場合もあるので、お使いのサービスのレートリミットに引っかかる可能性がないかは事前に検討してください。

ちなみに他の方法

ちなみに今回は自作しましたが、便利な gem もあります。 (@pocke さんに教えてもらいました!感謝!)

warning という gem を使うと、今回のようにキーワード引数の warning を String#include で判定することなく gem 側に任せることができます。

github.com

他にも同じ warning を再度出さないようにするなどの機能もあり、とても運用に優しいですね。 gem を新たに入れるということを許容できるのであれば、こちらを使用する選択肢もあると思いますので、ぜひ検討してみてください。

結果

結果的に今回の対応を本番にリリースしてみたところ、見逃していたものが数件見つかり修正して本番にリリースできました。 これをやっていなかったら、バージョンアップ時にエラーが出てた可能性もあったので、見つかってよかったです!

やはり本番で動いているコードを基に検知できるのが一番確実なので、Ruby 3 対応されている方はぜひ参考にしてみてください。

おまけ

ちなみに私が助かったドキュメントは pocke 先生の作品だったということが後でわかり、爆笑しました(ありがとう)。

github.com


マネーフォワード福岡拠点では、エンジニアを募集しています!

テクノロジードリブンでビジネス上の問題を解決したいエンジニアの方、応募お待ちしております!

hrmos.co

福岡開発拠点のサイトもあるのでぜひみてください!

fukuoka.moneyforward.com