ミツカリ技術ブログ

株式会社ミツカリの開発チームのブログです

rspecで特定のミドルウェアをSkipする方法

ミツカリのたなしゅん(@tanashun_dev)です。

ミツカリではこれまではCommitteeのRequestValidationを有効化していましたが、組織のコーディングルールとしてValidationをFormに統一しようということになったので、現在Formへの移行作業が行われています。

なぜFormへ統一することになったのかの経緯については先日、弊社CTOのつかびーさんが以下の記事を公開しておりますのでそちらを御覧ください。

tech-blog.mitsucari.com

廃止する方針になったからといって、いきなりRequestValidationのミドルウェアを削除してしまうと、Formが未実装な部分があったら大変なことになってしまいます。

幸い、弊社ではrspecを用いて、各Controllerの単体テストを整備しているので、単にミドルウェアを削除するとForm未実装な部分があったらspecがエラーになってくれることで多くを検知することが可能です。

試しにRequestValidationのミドルウェアを削除して全specを動かしてみると、、、、

大量に落ちました(そりゃそうじゃ)

これを全部直しきってからでないとRequestValidationのミドルウェアそのものを削除するのはまだ無理ですね。

ですが、ただのリファクタリングですし、これにエンジニアの工数を全ツッパして直すような類のタスクじゃないのは明らかです。

徐々に直していくしかないでしょう。

では直るまで直した部分はマージしないのか。

そんなことをしたらコンフリクトが大量に発生するでしょうし、そもそも新規の開発で新たにFormが不足したものが実装されてしまうかもしれません。

これを防ぎつつ徐々にリファクタリングする手段を考える必要がありますね。

rspec ミドルウェア 無効化 のような検索をしてみましたが、探しているような記事がヒットしませんでした。

単純にdeleteで削除できないものかと思い、以下を試してみました。

RSpec.describe Api::SampleController do
  include Committee::Rails::Test::Methods

  before :each do
    # ミドルウェアをセットするuseの逆でdeleteすればいいのかな?
    Rails.application.config.middleware.delete Committee::Middleware::RequestValidation
  end

  describe 'GET /index' do
    # ここにテストが実装される
  end
end

上記を実行すると以下のエラーが発生します。

FrozenError:
       can't modify frozen Array: [Committee::Middleware::RequestValidation]

ミドルウェアのuseは /config/application.rb に書きますが、そこではconfigがstackされていき、initializeが終わったあとはconfigの内容は書き換えできないように凍結されるという仕様のようです。

つまり、ミドルウェアのdeleteもstackからの削除でしかなく、実際に適用されたものから削除するコマンドではないということでした。

別のアプローチで解決できたのでそちらを紹介します。

RSpec.describe Api::SampleController do
  include Committee::Rails::Test::Methods

  before :each do
    # Committeeミドルウェアを無効化してテストする
    allow_any_instance_of(Committee::Middleware::RequestValidation).to receive(:call) do |middleware, env|
      # バリデーションをスキップして次のミドルウェアに直接渡す
      middleware.instance_variable_get(:@app).call(env)
    end
  end

  describe 'GET /index' do
    # ここにテストが実装される
  end
end

※詳細解説コメント入りのコードを記事の最後に載せています。

spec内でこのようにRequestValidationのミドルウェアをmockすることによってこのテストでのみRequestValidationのミドルウェアを削除したときと同じ挙動で実行することができます。

これにより、このControllerではFormの実装が適切にされているということが保証できます。

また、今後RequestValidationのミドルウェアを削除するまでに新たなAPIを実装した際も、このコードを仕込むことによって、新たに修正対象が増えることを防ぐことができますね。

RequestValidationのミドルウェアに限らず、何らかのミドルウェアを無効化したテストをしたい場合に流用可能だと思いますので、ぜひ参考にしてください。

このコードが誰かにとって有意義なものとなったら嬉しいです。

おまけ

紹介したコードの詳細解説コメント付きのものを載せておきます。

# 各exampleごとに実行(テスト間の副作用を避ける)
before :each do
  # Committeeのリクエストバリデーション用ミドルウェアを“実行時に”無効化する。
  # ここでは Rails のミドルウェアスタック自体を差し替えるのではなく、
  # RSpec のモック/スタブ機能でインスタンスメソッド :call を差し替えている。
  #
  # allow_any_instance_of:
  #   Committee::Middleware::RequestValidation のすべてのインスタンスの:call メソッドの挙動を置き換える。
  #
  # receive(:call) do |middleware, env|
  #   ブロック引数:
  #     - middleware: :call が呼ばれた対象インスタンス(any_instance_of の“実体”)
  #     - env: Rack 環境ハッシュ(Rack::Env)。アプリへ渡されるリクエストコンテキスト。
  #
  # 元の :call は JSON Schema によるリクエスト検証を行うが、
  # ここではそれをスキップし、次のミドルウェア(または最終アプリ)へ素通しさせる。
  allow_any_instance_of(Committee::Middleware::RequestValidation).to receive(:call) do |middleware, env|
    # Committee ミドルウェアは Rack 準拠で、@app に“次のミドルウェア(もしくはアプリ)”を保持している。
    # @app自体はpublicな変数としては公開されていないのでinstance_variable_get(:@app)で無理矢理参照する。
    # privateな変数に外からアクセスするのは本来はご法度だが、テストコードなので許容する。
    #
    # 本来はこのミドルウェア内で処理したenvを次のappにリレーする、
    # つまり、middleware.callの処理 => @app.callの処理 が連鎖していくが、
    # 何もせずに@app.callだけを行うようにする
    # これにより、Committee::Middleware::RequestValidation の処理だけがスキップされ、他のミドルウェアの挙動はそのままにできる
    middleware.instance_variable_get(:@app).call(env)
  end
end

現在、ミツカリではITエンジニアを募集しています。興味のある方はぜひお気軽にご連絡ください!

herp.careers