DEV Community

Cover image for System testing for Rails application without Capybara, using Puppeteer
Yusuke Iwaki
Yusuke Iwaki

Posted on • Edited on

System testing for Rails application without Capybara, using Puppeteer

Refer this article instead if you can read Japanese sentences.
日本語を読める方はこちらの記事を見てください

Background

Rails introduced "system testing", that makes it easy to configure Capybara: acceptance test framework for Ruby. We may often use it not for "acceptance test" but for just UI testing of the Rails application with React/Vue or other rich JavaScript-powered features.

Capybara DSL is actually a little inaccurate for working with pages with rich JavaScript features.

Capybara has internally polling with interval 10msec for waiting for Elements available and actionable.
https://github.com/teamcapybara/capybara/blob/0468de5a810aae75ab9de20447e246c5c35473f0/lib/capybara/node/base.rb#L91
I don't know this is really the main cause, but we may often face flaky/unstable testcases in using Capybara.

On the other hand, Puppeteer or Playwright have a relyable solution for waiting for Elements: Page#waitForSelector and Page#waitForNavigation. (Playwright has more advanced feature of auto-waiting )

I actually ported the libraries for using them in Rails application.

However the relyable features cannot be used from Capybara DSL. So let's try to configure system tests without Capybara, and using Puppeteer.

What happens if Capybara is absent??

Since Capybara is a huge framework, it would be difficult for some users to imagine what happens without Capybara.

Actually Rails application without Capybara loses the functionalities

  • Launching test HTTP server on starting tests
  • Capybara::DSL
    • Using visit or other methods raises NoMethodError instead of NotImplementedError
    • System spec (on RSpec) or SystemTestCase (on MiniTest), which depends on Capybara::DSL

However rspec-rails still provides features like

  • Attaching type: :feature type: :system to example.metadata
  • Defining feature background scenario methods for feature spec

So actually all we have to prepare is

  • Launching test HTTP server
  • Performing automation with browsers
    • This will be satisfied by Puppeteer

Launching test HTTP server without Capybara

Capybara is really kind that

  • finds available port
  • allows to configure host
  • allows to launch Webrick and Puma

However let's forget them at this moment, and assume that we only have to launch Puma with the baseURL http://127.0.0.1:3000

With referring the logics of Capybara preparing server and launching Puma server, then we can easily find the minimum configuration for launching test server like below:

RSpec.configure do |config| config.before(:suite) do # launching Rails server for system testing require 'rack/builder' testapp = Rack::Builder.app(Rails.application) do map '/__ping' do # debugging endpoint for heartbeat run ->(env) { [200, { 'Content-Type' => 'text/plain' }, ['OK']] } end end require 'rack/handler/puma' server_thread = Thread.new do Rack::Handler::Puma.run(testapp, Host: '127.0.0.1', Port: 3000, Threads: '0:4', workers: 0, daemonize: false, ) end # Waiting for Rails server is ready, using Net::HTTP require 'net/http' require 'timeout' Timeout.timeout(3) do loop do puts Net::HTTP.get(URI("http://127.0.0.1:3000/__ping")) break rescue Errno::EADDRNOTAVAIL sleep 1 rescue Errno::ECONNREFUSED sleep 0.1 end end end # Configure puppeteer-ruby for automation config.around(type: :feature) do |example| Puppeteer.launch(channel: :chrome, headless: false) do |browser| @server_base_url = 'http://127.0.0.1:3000' @puppeteer_page = browser.new_page example.run end end end 
Enter fullscreen mode Exit fullscreen mode

By putting this configuration into spec/support/integration_test_helper.rb or another place you prefer, now we can enjoy system testing with Puppeteer like below!!

As is already described, we cannot use system spec. Use feature spec (provided by rspec-rails) instead.

require 'rails_helper' describe 'example' do let(:base_url) { @server_base_url } let(:page) { @puppeteer_page } let(:user) { FactoryBot.create(:user) } it 'can browse' do page.goto("#{base_url}/tests/#{user.id}") page.wait_for_selector('input', visible: true) page.type_text('input', 'hoge') page.keyboard.press('Enter') text = page.eval_on_selector('#content', 'el => el.textContent') expect(text).to include('hoge') expect(text).to include(user.name) end end 
Enter fullscreen mode Exit fullscreen mode

integ_rails

A little refactoring for production use

The logic of launching test server, introduced in the previous section, is really straightforward. However most users would hesitate to copy/paste it into your own products because it's too dirty :(

No worry, here is a better code with more relyable Rack::Server.start to launch HTTP server, which is actually used in the implementation of rackup command

class RackTestServer def initialize(app:, **options) @options = options @options[:Host] ||= '127.0.0.1' @options[:Port] ||= 3000 require 'rack/builder' @options[:app] = Rack::Builder.app(app) do map '/__ping' do run ->(env) { [200, { 'Content-Type' => 'text/plain' }, ['OK']] } end end end def base_url "http://#{@options[:Host]}:#{@options[:Port]}" end def start require 'rack/server' Rack::Server.start(**@options) end def ready? require 'net/http' begin Net::HTTP.get(URI("#{base_url}/__ping")) true rescue Errno::EADDRNOTAVAIL false rescue Errno::ECONNREFUSED false end end def wait_for_ready(timeout: 3) require 'timeout' Timeout.timeout(3) do sleep 0.1 until ready? end end end 
Enter fullscreen mode Exit fullscreen mode

With this test server, the RSpec configuration file can be much shorter and simplified :)

RSpec.configure do |config| config.before(:suite) do # Launch Rails application test_server = RackTestServer.new( # options for Rack::Server # https://github.com/rack/rack/blob/2.2.3/lib/rack/server.rb#L173 app: Rails.application, server: :puma, Host: '127.0.0.1', Port: 3000, daemonize: false, # options for Rack::Handler::Puma # https://github.com/puma/puma/blob/v5.4.0/lib/rack/handler/puma.rb#L84 Threads: '0:4', workers: 0, ) Thread.new { test_server.start } test_server.wait_for_ready end # Configure puppeteer-ruby for automation config.around(type: :feature) do |example| Puppeteer.launch(channel: :chrome, headless: false) do |browser| @server_base_url = 'http://127.0.0.1:3000' @puppeteer_page = browser.new_page example.run end end end 
Enter fullscreen mode Exit fullscreen mode

Now, you can now get rid of Capybara from your app if you just want to launch HTTP server for testing :)

You can also use rack-test_server Gem instead of defining your own RackTestServer.

Top comments (0)