Ruby 外から Web アクセスする何か (Capybara とか、cli とか) を RSpec でテストするときのアクセス先をモックする
mmmpa.mmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmpa.net
のテストをこの方法で行いました。
本編
たとえば、Capybara
は各種ブラウザを介するアクセスのため、webmock
が効かず、別の gem が必要なのは有名です。 https://github.com/oesmith/puffing-billy
Capybara
でテストする際はこれでいいのですが、Capybara
をテストする場合には不便かもしれませんね。
また、バッククォートや Open3
を用いて呼びだすような cli、たとえば siege
などのターゲットをモックすることはできません。cli におんぶにだっこの gem を開発するときのテストに、かなり不便ですね。
RSpec
開始時にサーバーをたてる
望んだ結果を返すアクセス先があればいいので、テスト中に起動する仮想サーバーをたてます。
public
ディレクトリに適当な HTML を用意した上で、
RSpec.configure do |config| require 'webrick' config.before(:suite) do port = ENV['MOCK_PORT'] || 3000 host = ENV['MOCK_HOST'] || '127.0.0.1' started = false Thread.start do WEBrick::HTTPServer.new( DocumentRoot: File.expand_path('./public/', __dir__), BindAddress: host, Port: port, AccessLog: [], StartCallback: ->{ started = true } Logger: WEBrick::Log::new("/dev/null", 7) ).tap { |server| Signal.trap(:INT) { server.shutdown } server.start } end while !started sleep 0.5 end end end
Thread
はあたらしいものを用意しないと、sever.start
の時点で RSpec
のプロセスが停滞してしまいますので注意しましょう。
また、別 Thread
になるので server.start
が完了するしないにかかわらずテストに突入、テストが落ちるということがあるので、StartCallback
を使って、きちんとサーバーがスタートしたことを確認してからはじめましょう。
単純なアクセス以外
さらに、リダイレクトなど、単純な HTML アクセス以外の処理が必要ならば、インスタンス server
に mount
すると処理を追加できます。
# たとえばリダイレクト # Rails を模するなら、301(Moved) ではなく 302(Found) server.mount_proc('/redirect') do |req, res| res.set_redirect(WEBrick::HTTPStatus::Found, '/redirected.html') end
できあがり
これで、以下のようなテストが、自由に行えるようになりました。
expect(`siege -t 10s http://127.0.0.1:3000/foo.html`).to be_truthy
例は siege
自体のテストのようになってしまっていますが、実際は取得した結果をあれこれしたものをテストしました。
雑ではありますが、とても楽に、多くのアクセス先を用意できるようになりました。
siege をラップして多少細かい情報をまとめる gem 書いた。
Usage
たとえば Rails 内でこうやる。
re = SiegeSiege.run( time: 20, concurrent: 4, user_agent: false, urls: [ "http://localhost:3002#{students_path}", "http://localhost:3002#{students_path} POST name=abc", SiegeSiege::URL.new("http://localhost:3002#{students_path}", :post, {name: 'abc'}), ] + Student.ids.shuffle[0..2].map { |id| "http://localhost:3002#{student_path(id)}" } )
するとこういうのが得られる。
普通に使ってもコンソールに出てきてたやつ
re.total_result => {:command=>"siege -v -c 4 -t 20s -r 1 -R /tmp/20161030-21093-1lo07ca -f /tmp/20161030-21093-13f1kj6", :defaulting_to_time_based_testing=>{:value=>20.0, :unit=>"seconds"}, :transactions=>{:value=>96.0, :unit=>"hits"}, :availability=>{:value=>75.0, :unit=>"%"}, :elapsed_time=>{:value=>19.47, :unit=>"secs"}, :data_transferred=>{:value=>2.03, :unit=>"MB"}, :response_time=>{:value=>0.8, :unit=>"secs"}, :transaction_rate=>{:value=>4.93, :unit=>"trans/sec"}, :throughput=>{:value=>0.1, :unit=>"MB/sec"}, :concurrency=>{:value=>3.93, :unit=>""}, :successful_transactions=>{:value=>96.0, :unit=>""}, :failed_transactions=>{:value=>32.0, :unit=>""}, :longest_transaction=>{:value=>10.65, :unit=>""}, :shortest_transaction=>{:value=>0.01, :unit=>""}}
アクセスに使った URL 毎に平均レスポンス時間とか
re.average_log => [#<struct SiegeSiege::AverageLog id=6, url="http://localhost:3002/students/10763", count=17, secs=0.691, siege_url=#<SiegeSiege::URL:0x007fbee6efe870 @http_method=:get, @parameter={}, @url="http://localhost:3002/students/10763">>, #<struct SiegeSiege::AverageLog id=0, url="http://localhost:3002/students", count=16, secs=1.276, siege_url=#<SiegeSiege::URL:0x007fbee6efeb18 @http_method=:get, @parameter={}, @url="http://localhost:3002/students">>, #<struct SiegeSiege::AverageLog id=5, url="http://localhost:3002/students/39284", count=16, secs=0.767, siege_url=#<SiegeSiege::URL:0x007fbee6efe938 @http_method=:get, @parameter={}, @url="http://localhost:3002/students/39284">>, #<struct SiegeSiege::AverageLog id=7, url="http://localhost:3002/students/94576", count=17, secs=0.108, siege_url=#<SiegeSiege::URL:0x007fbee6efe7a8 @http_method=:get, @parameter={}, @url="http://localhost:3002/students/94576">>, #<struct SiegeSiege::AverageLog id=3, url="http://localhost:3002/students", count=15, secs=1.367, siege_url=#<SiegeSiege::URL:0x007fbee6cd9518 @http_method=:post, @parameter={:name=>"abc"}, @url="http://localhost:3002/students">>, #<struct SiegeSiege::AverageLog id=1, url="http://localhost:3002/students", count=15, secs=0.651, siege_url=#<SiegeSiege::URL:0x007fbee6efea50 @http_method=:post, @parameter="name=abc", @url="http://localhost:3002/students">>]
標準出力をまとめたやつ
re.raw_log => [#<struct SiegeSiege::LineLog protocol="HTTP/1.1", status="200", secs=10.09, bytes=633, url="http://localhost:3002/students/10763", id=6, date=Sun, 30 Oct 2016 13:10:31 +0000, siege_url=#<SiegeSiege::URL:0x007fbee6efe870 @http_method=:get, @parameter={}, @url="http://localhost:3002/students/10763">>, #<struct SiegeSiege::LineLog protocol="HTTP/1.1", status="200", secs=10.29, bytes=45579, url="http://localhost:3002/students", id=0, date=Sun, 30 Oct 2016 13:10:31 +0000, siege_url=#<SiegeSiege::URL:0x007fbee6efeb18 @http_method=:get, @parameter={}, @url="http://localhost:3002/students">>, #<struct SiegeSiege::LineLog protocol="HTTP/1.1", status="200", secs=10.24, bytes=633, url="http://localhost:3002/students/39284", id=5, date=Sun, 30 Oct 2016 13:10:31 +0000, siege_url=#<SiegeSiege::URL:0x007fbee6efe938 @http_method=:get, @parameter={}, @url="http://localhost:3002/students/39284">>, #<struct SiegeSiege::LineLog protocol="HTTP/1.1", status="200", secs=0.25, bytes=633, url="http://localhost:3002/students/94576", id=7, date=Sun, 30 Oct 2016 13:10:41 +0000, siege_url=# # 略
所感
カジュアルにテストできて便利ですね、みたいなツールで、詳細な情報を取ろうとするのはやめといた方がいいと思いました。(適切なやつをさがそう)
Capybara で Chromedriver をつかってモバイルモードでテストする時の Capybara.register_driver とか
だいたいのサイトは User Agent で切り替えてると思うので、わざわざモバイルモードでテストが必要なのかしらとか思わないでもない (ダブルタップとか、スワイプをテストする?)、が一応メモ。
Capybara に Driver として登録
# chromedriver configuration # chrome の起動オプションが使える http://peter.sh/experiments/chromium-command-line-switches/ # デフォルトではサブのディスプレイに表示してしまうので、ずらしている default_args = %w( --window-position=2560,0 ) # ブラウザの外枠 (スクリーンショット撮って計測しよう) chrome_frame_offset = { w: 10, h: 86 } # プリセットの商品のセッティングを使う場合。 # DevTools で選べるモバイルから必要なものをピックアップ。(リストデータくれよ) [ {name: 'Apple iPhone 6 Plus', w: 414, h: 736}, # :apple_iphone_6_plus {name: 'Google Nexus 7', w: 600, h: 960}, # :google_nexus_7 ].map { |configure| configure[:w] += chrome_frame_offset[:w] configure[:h] += chrome_frame_offset[:h] configure }.each do |configure| Capybara.register_driver configure[:name].gsub(' ', '_').downcase.to_sym do |app| Capybara::Selenium::Driver.new( app, browser: :chrome, desired_capabilities: Selenium::WebDriver::Remote::Capabilities.chrome( 'chromeOptions' => { args: [ "--window-size=#{configure[:w]},#{configure[:h]}" ] + default_args, mobileEmulation: {deviceName: configure[:name]} } ) ) end end # 独自のセッティングでモバイルモードを使う場合 # これはイッパツで全画面をスクリーンショットしたかった時の設定 Capybara.register_driver :chrome_dummy_mobile do |app| w = 320 h = 1600 Capybara::Selenium::Driver.new( app, browser: :chrome, desired_capabilities: Selenium::WebDriver::Remote::Capabilities.chrome( 'chromeOptions' => { args: [ "--window-size=#{w + chrome_frame_offset[:w]},#{h + chrome_frame_offset[:h]}" ] + default_args, mobileEmulation: { deviceMetrics: { width: w, height: h, pixelRatio: 2.0 }, userAgent: 'Mozilla/5.0 (iPhone; CPU iPhone OS 9_3_2 like Mac OS X) AppleWebKit/601.1.46 (KHTML, like Gecko) Version/9.0 Mobile/13F69 Safari/601.1' } } ) ) end
選んで使う
# 登録しておいた driver から選ぶ Capybara.configure do |config| config.run_server = false config.default_driver = :apple_iphone_6_plus config.app_host = 'http://googole.com/' end
RSpec とか Capybara の Example 内で使いたいメソッドをクラス単位でまとめて使いやすくする gem を書いた。
これ
先週の gem は 80 行ぐらいだったんですが、今回は 40 行です。
なぜ
入社してからこっち、ずっと Vue.js で SPA 制作という業務に従事していましたが、UI 系の宿命で (一部しか) テストがない。
そんななかで、わりと分岐の多い (難しくはない) 実装をすることになって、これは手作業では (変更の都度に) 確認しきれんなということで Capybara に出張ってもらっていました。テスト維持コスト問題というものがあるので、業務リポジトリには入っていない、ローカルでのみ存在する Capybara です。かわいいよ。
そんななかで find
や all
を直書きしていると、途中での変更抜きにしても、見栄えがとてもつらいことになります。
経路によっては他の Example にも出てくるので、メソッド化をするわけですが、しかし、ページごとにちがう submit_button
があったりするので、 registration_page_submit_button
や shopping_cart_submit_button
など、なんとかして名前をかえていく必要がありまして、これが読みづらい。
というわけで Registration.submit_button
や ShoppingCart.submit_button
というように簡単にわけられるようにしました。
例
これが
def registration_page_submit_button find('button', text: 'Register Now!') end def shopping_cart_submit_button find('button', text: 'Buy Now!') end feature 'register then buy' do scenario do visit '/resistration' # input data to some form registration_page_submit_button.click visit '/cart' # input data to some form shopping_cart_submit_button.click end end
こうなる
class RegistrationPage < RSpecMethodGrouping::Base def submit_button find('button', text: 'Register Now!') end end
class ShoppingCart < RSpecMethodGrouping::Base def submit_button find('button', text: 'Buy Now!') end end
feature 'register then buy' do scenario do visit '/resistration' # input data to some form RegistrationPage.submit_button.click visit '/cart' # input data to some form ShoppingCart.submit_button.click end end
よみやすい!
かどうかは人によるかと思いますが、メソッドの管理がしやすくなったのでヨシとします。
この量ではわかりにくいかもしれませんが、他画面遷移 + 要素めちゃ多いとかになると、結構効いてきます。
テスト内の隠蔽どうなの問題
テストに使うデータを過度に隠蔽すると、テストの意図まで隠蔽されて、まるでわからないという問題があります。
しかし、これは、テストの主題ではなく、ページ内の要素を特定しておく (変更時はイッパツで対応できる) のが主目的なので、特に問題ないと思います。
もりもり Capybara でスクリーンショットを撮っていきましょう。
最近 SQL にはまっているので、ついでに Rails というか ActiveRecord で発行された SQL query をカウントする gem を書いた。
Installation
gem 'a_r_q_logger'
せつめい
ActiveRecord::Base
サブクラスのインスタンス生成をできるだけ抑えれば、それだけ処理時間が抑えられるのは自明とされています。
そこで、今は、できるだけ少ない queries に抑えることを目的として SQL の練習をしていますので、すぐわかるようにしました。
その練習を進めていく上で、その 1 query の処理時間が気になっており、SQL のみの処理時間もとれるようにしましたというか、出発点はこっちです。
pry(main)> ARQLogger.log { 3.times { TestModel.has_children(10).map(&:children_count) } } => #<struct ARQLogger::Result count=3, msec=1508.4>
from
句で前もって as
しておくのと、CASE
の WHEN
ごとに (複数の) 集約関数を書くのではどっちが速いのか気になって、こういうことをしていたのです。 (おそらくオプティマイザがいい感じにしてくれるおかげで、CASE
に同じ式を何回も書くほうが速かった)
テストでも使える
query を抑えようと書いたかっこいい生 SQLが、善意の第三者により書きかえられるということはありがちなことですが、amount of queries を抑えようと書いた生 SQL が、その善意により too many queries になっては目もあてられません。
query 発行量はコントロールできる要素ですから、これはテストで縛っておくことができます。
たとえば以下は N + 1 の解決法でよく出てくる includes
の付与テストです。
it do expect(ARQLogger.log { TestModel.includes(:test_child_models).all.each { |m| m.test_child_models.map(&:name) } }.count).to eq(2) end
it do expect(ARQLogger.log { TestModel.all.each { |m| m.test_child_models.map(&:name) } }.count).to eq(11) end
久しぶりに gem を書くのが目的だった
どこかにありそうだなと思って軽く調べて、そんな感じの gem もありましたが軽く無視って書きました。
たのしかたです。
npm run scripts で current directory を参照したい。
環境変数を使います。
$PWD
ではダメ
npm run foo
は常に基準となるディレクトリで実行されるので、たとえば以下の npm run here
は、配下のディレクトリに潜っても同じ結果を返します。
"scripts": { "here": "echo $PWD" }
これは current directory を考慮せずに node_modules を一定の条件で使えるので、とても便利です。
なので先に設定しておく
便利ですが、 current directory が欲しい場合もありますので、別個に環境変数を設定し、それを参照します。
"scripts": { "here": "echo $C_D" }
$ C_D=$PWD npm run build
これで実行したディレクトリを得られます。(しかし調べればビルトインで参照できる変数がありそうである)
おわり。
なぜ
watchify
いまは watchify
でコンパイル行為を行なっており、こういう build
を用意しています。
"scripts": { "build": "watchify -t babelify --extension=js ./index.js -o ./built.js -v" }
特定のディレクトリを指定して watchify
SPA を作成している時分にはこれでよかったのですが、今は自習として、共通のライブラリを使う別々の小さな js ファイルを作成するということをやっています。
いっぱいあるし、個々の build
は書きたくない。
このようにしておいて
"scripts": { "build": "watchify -t babelify --extension=js $C_D/index.js -o $C_D/built.js -v" }
$ C_D=$PWD npm run build
余分な js を書く必要がなくてハッピーでした。(脱 Gulp
中かつ、なんらかの他のランナーも使う予定がなかった)
就職できました。
ところで先日カピバラ神に詣でた時の写真です。これもうあれでしょ、完全に俺のための俺視線。 pic.twitter.com/kDkHs39mlH
— おふくろさま (@o296sm) May 18, 2016
前いったときおもくそケツむけてお食事中だったけど時期が悪かったんや。いまこそ就職活動をはじめる時期だという啓示かなんかだろこれわ。
— おふくろさま (@o296sm) May 18, 2016
1年間の無職を終えました。
HeartRails | ハートレイルズ | ザ・ウェブサービス・カンパニー
よろしくおねがいします。