Railsでの自動コントローラーテスト

こんにちは、Habr! 月桂樹は長い間私を作家に招き、今やついに、あなたの法廷に私の小さな作品を提出することを終えたとき、その光の時が来ました。



余暇にRubyとRailsを勉強し、RSpecを試し、次にMinitestを試し、通常のRailsコントローラーに基づいてREST APIを突き出したJavaScriptフロントエンドとRubyバックエンドを備えたWebアプリケーションを作成しました。 Railsを使用していますが、これはまったく重要ではありません。 以下に説明するアプローチは、あらゆるものに適用できます。



ここで小さな余談をする必要があります。 本質的に、私は要求の厳しい人物であり、あらゆる点で、コードの実証済みの安定性に苦労しています(確かに)。 そして、少なくとも私のデータを受け取らないだろうということを示す、テストなしの私のアプリケーションのユーザーの安全に関しては、私は完全に不快に感じます。 私は悲しく感じ始め、コードをまったく書きません。 私がこれまでのところ唯一のユーザーであっても。



すべてが非常に単純であるように思えます。RSpecを取得し、テストを記述します。 しかし、これは退屈です! コントローラごとに、サポートされているメソッドごとに、少なくとも以前に発行されたトークンがなければ、ユーザーがゲートからターンを受け取ることを確認します。これは、同じコードをどれだけ書く必要があるかです! そして、どうやって? より多くのコントローラーがあり、テストをコピーするのは退屈であり、アプローチを変える可能性には限りがあります。 たとえば、APIのバージョンをURLではなくヘッダーで、またはその逆に転送する場合は、これらすべてのテストを書き直してください。 一般的に、ジェネレーターを作成することにしました。



問題の声明



既存の各コントローラーについて、2つのテストケースがありました。アクセストークンなしでアクセスしようとすると、アプリケーションはコード401を返し、存在しないアクセストークンを使用して-コードは403です。これらのルールが守られている場合、正しいアクセストークンを使用すると、アプリケーションはこのトークンの所有者情報を提供し、他のトークンは提供しませんが、これはこの記事の範囲外です。 つまり、次のようなものがありました。



describe Api::V2::UserSessionsController do let (:access_token) {SecureRandom.hex(64)} describe 'DELETE /user-sessions/:id' do context 'without an access token' do before { delete :destroy, id: access_token } it 'responds with 401' do expect(response).to have_http_status :unauthorized end end context 'with non-existent access token' do before {@request.headers['X-API-Token'] = SecureRandom.hex(64)} before {delete :destroy, id: access_token} it 'responds with 403' do expect(response).to have_http_status :forbidden end end end end
      
      





まあ、これを2回以上書かないという欲求。



どうする



Rubyは豊富なメタプログラミング言語です。 それらのおかげで、特に、RSpec DSLがあり、その使用は上記の例で示されています。 RSpec DSLとは何ですか? ライブラリが単純に起動するクラスを書くためのシュガー。 したがって、それらを自分で生成できます! 幸いなことに、すべてのコントローラーに1つの基本クラスしかないため、この問題を解決することは既に技術的な問題です。 Google、StackOverflow、自分の頭を少し苦しめたので(彼女はまだタスクを設定する必要はありません-彼女もそれらを解決する必要があります)、私はこのコードに行きました:



 describe Api::V2::ControllerBase do Api::V2::ControllerBase.descendants.each do |c| describe "#{c.name}" do Rails.application.routes.set.each do |route| if route.defaults[:controller] == c.controller_path action = route.defaults[:action] request_method = /[AZ]+/.match(route.constraints[:request_method].to_s)[0] param_placeholders = Hash[route.parts.reject { |p| p == :format }.map { |p| [p, ":#{p}"] }] spec_name = "#{request_method} #{route.format(param_placeholders)}" describe "#{spec_name}" do before { self.controller = c.new } context 'without an access token' do before { process(action, request_method, param_placeholders) } it 'responds with 401' do expect(response).to have_http_status :unauthorized end end context 'with non-existent access token' do before { @request.headers['X-API-Token'] = SecureRandom.hex(64) } before { process(action, request_method, param_placeholders) } it 'responds with 403' do expect(response).to have_http_status :forbidden end end end end end end end end
      
      





ただし、このコードが機能しないことをお急ぎください。



どうして?



とても簡単です。 遅延読み込みがすべてです。 Railsはメモリに不要なものを散らばらせません。 したがって、Api :: V2 :: ControllerBaseの子孫配列は空です。 幸いなことに、これは簡単に処理できます。



 Rails.application.eager_load!
      
      





最初の記述の後に挿入されたこの魔法の行は、状況を逆さまにします。



画像



RubyMineを使用するためのvimとコンソールの愛好者を許してください。



書いて忘れた



元々のアイデアは、セキュリティの基本を心配せずに落ち着いて作成することでした。 コントローラを追加すると、緑のテストの数が魔法のように増えます。 しかし、上の写真が示すように、それらは常にすぐに緑色になるとは限りません。



一般的なルールの対象ではないメソッドがあります。 C'est la vie。 私の場合、たとえば、POST / api / user-sessions /です。これを呼び出すメソッドから正しいアクセストークンを要求するのは愚かだからです。 ためらうことなく、私はこれを追加しました:



  def self.excluded_actions { Api::V2::UserSessionsController => [:create], Api::V2::UserCalendarsController => [:oauth2callback_no_ajax] } end
      
      





そしてこれ:

 next if excluded_actions.key?(c) && excluded_actions[c].include?(action.to_sym)
      
      





メタテストのコードに。 すべてがすぐに緑色になりました。



確かに、今ではそれが機能しないことを完全に忘れます。



おわりに



Rubyはメタプログラミング機能が優れており、RSpecは理解が優れています(生成されたテストケースをすぐに生成して実行できるとは思いませんでした)が、Railsは定義からすれば優れています。 適切な器用さで、テストケースの自動生成を使用して、テストコードの可読性を失うことなく、さまざまな問題を解決できます。 このソリューションは誰かに役立つと確信しています。



ご清聴ありがとうございました。



PSネタバレの下に最終的なソリューションコードを隠しました。



最終的なメタテストコード
 describe Api::V2::ControllerBase do Rails.application.eager_load! def self.excluded_actions { Api::V2::UserSessionsController => [:create], Api::V2::UserCalendarsController => [:oauth2callback_no_ajax] } end Api::V2::ControllerBase.descendants.each do |c| describe "#{c.name}" do Rails.application.routes.set.each do |route| if route.defaults[:controller] == c.controller_path action = route.defaults[:action] next if excluded_actions.key?(c) && excluded_actions[c].include?(action.to_sym) request_method = /[AZ]+/.match(route.constraints[:request_method].to_s)[0] param_placeholders = Hash[route.parts.reject { |p| p == :format }.map { |p| [p, ":#{p}"] }] spec_name = "#{request_method} #{route.format(param_placeholders)}" describe "#{spec_name}" do before { self.controller = c.new } context 'without an access token' do before do process(action, request_method, param_placeholders) end it 'responds with 401' do expect(response).to have_http_status :unauthorized end end context 'with non-existent access token' do before {@request.headers['X-API-Token'] = SecureRandom.hex(64)} before { process(action, request_method, param_placeholders) } it 'responds with 403' do expect(response).to have_http_status :forbidden end end end end end end end end
      
      







PPS Elisabeth Hendricksonのアイデアと例に感謝します。



All Articles