Closed5

Capybaraとrspec-rails

Yusuke IwakiYusuke Iwaki
Gemfile
group :test do
  gem 'rspec-rails'
end

あえてCapybaraなし

$ bundle exec rails g rspec:install
Running via Spring preloader in process 43250
      create  .rspec
      create  spec
      create  spec/spec_helper.rb
      create  spec/rails_helper.rb
spec/features/example_spec.rb
require 'rails_helper'

describe 'example' do
  it { |example|
    require 'pry'
    binding.pry
  }
end

止めてみる。

$ bundle exec rspec spec/features/example_spec.rb 

From: /Users/yusuke-iwaki/Desktop/railsfeature/spec/features/example_spec.rb:6 :

    1: require 'rails_helper'
    2: 
    3: describe 'example' do
    4:   it { |example|
    5:     require 'pry'
 => 6:     binding.pry
    7:   }
    8: end

[1] pry(#<RSpec::ExampleGroups::Example>)> example.metadata
=> {:block=>#<Proc:0x00007fe95746bf98 /Users/yusuke-iwaki/Desktop/railsfeature/spec/features/example_spec.rb:4>,
 :description_args=>[],
 :description=>"",
 :full_description=>"example ",
 :described_class=>nil,
 :file_path=>"./spec/features/example_spec.rb",
 :line_number=>4,
 :location=>"./spec/features/example_spec.rb:4",
 :absolute_file_path=>"/Users/yusuke-iwaki/Desktop/railsfeature/spec/features/example_spec.rb",
 :rerun_file_path=>"./spec/features/example_spec.rb",
 :scoped_id=>"1:1",
 :type=>:feature,
 :execution_result=>
  #<RSpec::Core::Example::ExecutionResult:0x00007fe95746bca0 @started_at=2021-07-21 21:19:54.470283 +0900>,
 :example_group=>
  {:block=>#<Proc:0x00007fe95744a960 /Users/yusuke-iwaki/Desktop/railsfeature/spec/features/example_spec.rb:3>,
   :description_args=>["example"],
   :description=>"example",
   :full_description=>"example",
   :described_class=>nil,
   :file_path=>"./spec/features/example_spec.rb",
   :line_number=>3,
   :location=>"./spec/features/example_spec.rb:3",
   :absolute_file_path=>"/Users/yusuke-iwaki/Desktop/railsfeature/spec/features/example_spec.rb",
   :rerun_file_path=>"./spec/features/example_spec.rb",
   :scoped_id=>"1",
   :type=>:feature},
 :shared_group_inclusion_backtrace=>[],
 :last_run_status=>"unknown"}

https://github.com/rspec/rspec-rails/blob/fe95eacb376f7fa558d3ffeeaa9afa6d3110a540/lib/rspec/rails/configuration.rb#L38 ここでrspec-railsが指定している type: :feature が付与されているが、サーバーなどは特に起動してなさそう。

Capybara::DSLのロードは Capybara側でやっているが、今回はCapybaraを入れていないので、この処理は走らない。
https://github.com/teamcapybara/capybara/blob/master/lib/capybara/rspec.rb

RSpec.configure do |config|
  config.include Capybara::DSL, type: :feature
  config.include Capybara::RSpecMatchers, type: :feature
  config.include Capybara::DSL, type: :system
  config.include Capybara::RSpecMatchers, type: :system
  config.include Capybara::RSpecMatchers, type: :view
Yusuke IwakiYusuke Iwaki

RailsのSystemTestCase側からどうなってるか見てみる。

https://github.com/rails/rails/blob/v6.1.4/actionpack/lib/action_dispatch/system_test_case.rb

actionpack/lib/action_dispatch/system_test_case.rb
    def self.start_application # :nodoc:
      Capybara.app = Rack::Builder.new do
        map "/" do
          run Rails.application
        end
      end

      SystemTesting::Server.new.run
    end
actionpack/lib/action_dispatch/system_testing/server.rb
      def run
        setup
      end

      private
        def setup
          set_server
          set_port
        end

        def set_server
          Capybara.server = :puma, { Silent: self.class.silence_puma } if Capybara.server == Capybara.servers[:default]
        end

        def set_port
          Capybara.always_include_port = true
        end

ちなみにfeature specではRailsのSystemTestCaseではなくrspec-railsが require 'capybara/rails' した先で、似たようなことをやっている。
https://github.com/teamcapybara/capybara/blob/master/lib/capybara/rails.rb

lib/capybara/rails.rb
require 'capybara/dsl'

Capybara.app = Rack::Builder.new do
  map '/' do
    run Rails.application
  end
end.to_app
Yusuke IwakiYusuke Iwaki

Capybaraを入れるしかなさそうだが、Capybaraが入っていると

rspec-railsが勝手に Feature とか System specだと勝手にCapybara::DSLまで入れてしまう。
https://github.com/rspec/rspec-rails/blob/main/lib/rspec/rails.rb

require 'rspec/rails/vendor/capybara'

https://github.com/rspec/rspec-rails/blob/main/lib/rspec/rails/vendor/capybara.rb

begin
  require 'capybara/rspec'
rescue LoadError
end

begin
  require 'capybara/rails'
rescue LoadError
end

if defined?(Capybara)
  RSpec.configure do |c|
    if defined?(Capybara::DSL)
      c.include Capybara::DSL, type: :feature
      if defined?(ActionPack) && ActionPack::VERSION::STRING >= "5.1"
        c.include Capybara::DSL, type: :system
      end
    end
$ bundle exec rspec spec/features/example_spec.rb 

From: /Users/yusuke-iwaki/Desktop/railsfeature/spec/features/example_spec.rb:6 :

    1: require 'rails_helper'
    2: 
    3: describe 'example' do
    4:   it { |example|
    5:     require 'pry'
 => 6:     binding.pry
    7:   }
    8: end

[1] pry(#<RSpec::ExampleGroups::Example>)> Capybara
=> Capybara
[2] pry(#<RSpec::ExampleGroups::Example>)> method(:visit)
=> #<Method: RSpec::ExampleGroups::Example(RSpec::Rails::FeatureExampleGroup)#visit(*) /Users/yusuke-iwaki/.rbenv/versions/3.0.0/lib/ruby/gems/3.0.0/gems/rspec-rails-5.0.1/lib/rspec/rails/example/feature_example_group.rb:27>
[3] pry(#<RSpec::ExampleGroups::Example>)> visit '/'
ActionController::RoutingError: No route matches [GET] "/"
from /Users/yusuke-iwaki/.rbenv/versions/3.0.0/lib/ruby/gems/3.0.0/gems/actionpack-6.1.4/lib/action_dispatch/middleware/debug_exceptions.rb:33:in `call'
Yusuke IwakiYusuke Iwaki

spec/features をspec/integration にリネームしてみる。

[1] pry(#<RSpec::ExampleGroups::Example>)> example.metadata
=> {:block=>#<Proc:0x00007faf027f9898 /Users/yusuke-iwaki/Desktop/railsfeature/spec/integration/example_spec.rb:4>,
 :description_args=>[],
 :description=>"",
 :full_description=>"example ",
 :described_class=>nil,
 :file_path=>"./spec/integration/example_spec.rb",
 :line_number=>4,
 :location=>"./spec/integration/example_spec.rb:4",
 :absolute_file_path=>"/Users/yusuke-iwaki/Desktop/railsfeature/spec/integration/example_spec.rb",
 :rerun_file_path=>"./spec/integration/example_spec.rb",
 :scoped_id=>"1:1",
 :type=>:request,
 :execution_result=>
  #<RSpec::Core::Example::ExecutionResult:0x00007faf027f9348 @started_at=2021-07-21 22:40:45.25276 +0900>,
 :example_group=>
  {:block=>#<Proc:0x00007faf027a5c20 /Users/yusuke-iwaki/Desktop/railsfeature/spec/integration/example_spec.rb:3>,
   :description_args=>["example"],
   :description=>"example",
   :full_description=>"example",
   :described_class=>nil,
   :file_path=>"./spec/integration/example_spec.rb",
   :line_number=>3,
   :location=>"./spec/integration/example_spec.rb:3",
   :absolute_file_path=>"/Users/yusuke-iwaki/Desktop/railsfeature/spec/integration/example_spec.rb",
   :rerun_file_path=>"./spec/integration/example_spec.rb",
   :scoped_id=>"1",
   :type=>:request},
 :shared_group_inclusion_backtrace=>[],
 :last_run_status=>"unknown"}
[2] pry(#<RSpec::ExampleGroups::Example>)> Capybara
=> Capybara
[3] pry(#<RSpec::ExampleGroups::Example>)> method(:visit)
NameError: undefined method `visit' for class `#<Class:#<RSpec::ExampleGroups::Example:0x00007faf08878ef8>>'
from (pry):3:in `method'

[12] pry(#<RSpec::ExampleGroups::Example>)> Capybara.current_session
=> #<Capybara::Session>
[13] pry(#<RSpec::ExampleGroups::Example>)> Capybara.current_session.visit('/')
ActionController::RoutingError: No route matches [GET] "/"
from /Users/yusuke-iwaki/.rbenv/versions/3.0.0/lib/ruby/gems/3.0.0/gems/actionpack-6.1.4/lib/action_dispatch/middleware/debug_exceptions.rb:33:in `call'
  • Capybara.app にはRailsアプリケーションRack指定されている
    • rspec-railsがrequire capybara/railsしていて、その中でやってるから
  • Capybara::DSLは入っていない状態
    • DSLがないだけで、 Capybara.current_session から操作はできる
  • サーバーは起動していない

https://github.com/teamcapybara/capybara/blob/master/lib/capybara/session.rb

      @server = if config.run_server && @app && driver.needs_server?
        server_options = { port: config.server_port, host: config.server_host, reportable_errors: config.server_errors }
        server_options[:extra_middleware] = [Capybara::Server::AnimationDisabler] if config.disable_animation
        Capybara::Server.new(@app, **server_options).boot
      end
[8] pry(#<RSpec::ExampleGroups::Example>)> Capybara.current_session.driver.needs_server?
=> false

サーバーを無理やり起動させるべく、カスタムドライバを入れる。

spec/integration/example_spec.rb
describe 'example' do
  class NullDriver < Capybara::Driver::Base
    def needs_server?
      true
    end
  end

  before(:all) do
    Capybara.server = :puma, { Silent: false }
    Capybara.always_include_port = true
    Capybara.register_driver(:null) { NullDriver.new }
  end

  around do |example|
    Capybara.current_driver = :null
    example.run
    Capybara.use_default_driver
  end

  it { |example|
    require 'pry'
    binding.pry
  }
end
[1] pry(#<RSpec::ExampleGroups::Example>)> Capybara.current_session
Capybara starting Puma...
* Version 5.3.2 , codename: Sweetnighter
* Min threads: 0, max threads: 4
* Listening on http://127.0.0.1:61811
=> #<Capybara::Session>
[2] pry(#<RSpec::ExampleGroups::Example>)> 

sessionオブジェクトは初回アクセス時に作られるっぽいので、 Capybara.current_session が参照された時点でサーバーが起動する。

このスクラップは2021/07/23にクローズされました