|
1 | 1 | # Capybara & Rspec \[WIP\]
|
2 | 2 |
|
| 3 | +Matestack apps, pages and components can be tested with various test setups. We're using Rspec and Capybara a lot when creating apps with Matestack or work on Matestack's core itself and want to show you some basic elements of this setup. |
| 4 | + |
| 5 | +We will show you how to setup a **headless chrome** for testing, because a headless browser approach gives you performance benefits and is better suited to be integrated in a CI/CD pipeline. |
| 6 | + |
| 7 | +## Setup |
| 8 | + |
| 9 | +In this guide we assume that you know the basics of Rspec and Capybara and have both gems installed. If not, please read the basics about these tools here: |
| 10 | + |
| 11 | +* [https://github.com/rspec/rspec-rails](https://github.com/rspec/rspec-rails) |
| 12 | +* [https://github.com/teamcapybara/capybara](https://github.com/teamcapybara/capybara) |
| 13 | + |
| 14 | +Additionally you need a Chrome browser installed on your system. |
| 15 | + |
| 16 | +We recommend to configure Capybara in a separate file and require it in your `rails_helper.rb` |
| 17 | + |
| 18 | +{% tabs %} |
| 19 | +{% tab title="spec/rails\_helper.rb" %} |
| 20 | +```ruby |
| 21 | +# This file is copied to spec/ when you run 'rails generate rspec:install' |
| 22 | +require "spec_helper" |
| 23 | +ENV["RAILS_ENV"] ||= "test" |
| 24 | +require File.expand_path("../config/environment", __dir__) |
| 25 | +# Prevent database truncation if the environment is production |
| 26 | +abort("The Rails environment is running in production mode!") if Rails.env.production? |
| 27 | +require "rspec/rails" |
| 28 | + |
| 29 | +Dir[File.join File.dirname(__FILE__), "support", "**", "*.rb"].each { |f| require f } |
| 30 | +# Add additional requires below this line. Rails is not loaded until this point! |
| 31 | +``` |
| 32 | +{% endtab %} |
| 33 | + |
| 34 | +{% tab title="spec/support/capybara.rb" %} |
| 35 | +```ruby |
| 36 | +require "capybara/rspec" |
| 37 | +require "capybara/rails" |
| 38 | +require "selenium/webdriver" |
| 39 | + |
| 40 | +# port used for debugging (explained later) |
| 41 | +Capybara.server_port = 33123 |
| 42 | +Capybara.server_host = "0.0.0.0" |
| 43 | + |
| 44 | +Capybara.register_driver :headless_chrome do |app| |
| 45 | + chrome_options = Selenium::WebDriver::Chrome::Options.new.tap do |o| |
| 46 | + o.add_argument "--headless" |
| 47 | + o.add_argument "--no-sandbox" |
| 48 | + o.add_argument "--disable-dev-shm-usage" |
| 49 | + o.add_argument "--disable-gpu" |
| 50 | + o.add_argument "--enable-features=NetworkService,NetworkServiceInProcess" |
| 51 | + end |
| 52 | + Capybara::Selenium::Driver.new(app, browser: :chrome, options: chrome_options) |
| 53 | +end |
| 54 | +Capybara.default_driver = :headless_chrome |
| 55 | + |
| 56 | +``` |
| 57 | +{% endtab %} |
| 58 | +{% endtabs %} |
| 59 | + |
| 60 | +## Writing basic specs |
| 61 | + |
| 62 | +Imagine having implemented a Matestack page like: |
| 63 | + |
| 64 | +{% tabs %} |
| 65 | +{% tab title="app/matestack/some\_page.rb" %} |
| 66 | +```ruby |
| 67 | +class SomePage < Matestack::Ui::Page |
| 68 | + |
| 69 | + def response |
| 70 | + plain "hello world!" |
| 71 | + end |
| 72 | + |
| 73 | +end |
| 74 | +``` |
| 75 | +{% endtab %} |
| 76 | + |
| 77 | +{% tab title="app/controllers/some\_controller.rb" %} |
| 78 | +```ruby |
| 79 | +class SomeController < ApplicationController |
| 80 | + |
| 81 | + include Matestack::Ui::Core::Helper |
| 82 | + |
| 83 | + def some_page |
| 84 | + render SomePage |
| 85 | + end |
| 86 | + |
| 87 | +end |
| 88 | +``` |
| 89 | +{% endtab %} |
| 90 | + |
| 91 | +{% tab title="config/routes.rb" %} |
| 92 | +```ruby |
| 93 | +Rails.application.routes.draw do |
| 94 | + |
| 95 | + get 'some_page', to: 'some#some_page' |
| 96 | + |
| 97 | +end |
| 98 | +``` |
| 99 | +{% endtab %} |
| 100 | +{% endtabs %} |
| 101 | + |
| 102 | +A spec might look like this: |
| 103 | + |
| 104 | +{% tabs %} |
| 105 | +{% tab title="spec/features/hello\_world\_spec.rb" %} |
| 106 | +```ruby |
| 107 | +require "rails_helper" |
| 108 | + |
| 109 | +describe "Some Page", type: :feature do |
| 110 | + |
| 111 | + it "should render hello world" do |
| 112 | + visit some_page_path |
| 113 | + expect(page).to have_content("hello world!") |
| 114 | + end |
| 115 | + |
| 116 | +end |
| 117 | + |
| 118 | +``` |
| 119 | +{% endtab %} |
| 120 | +{% endtabs %} |
| 121 | + |
| 122 | +and then run this spec with `bundle exec rspec spec/features/hello_world_spec.rb` |
| 123 | + |
| 124 | +This should start a webserver and trigger the headless chrome to request the specified page from it. Just like Capybara is working. |
| 125 | + |
| 126 | +## Testing asynchronous features |
| 127 | + |
| 128 | +{% hint style="info" %} |
| 129 | +Above, we just tested a static "hello world" rendering and didn't use any JavaScript based functionality. We need to activate the JavaScript driver in specs where Matestack's built-in \(or your own\) JavaScript is required. |
| 130 | +{% endhint %} |
| 131 | + |
| 132 | +Let's add some basic built-in reactivity of Matestack, which requires JavaScript to work: |
| 133 | + |
| 134 | +```ruby |
| 135 | +class SomePage < Matestack::Ui::Page |
| 136 | + |
| 137 | + def response |
| 138 | + onclick emit: "show_hello" do |
| 139 | + button "click me" |
| 140 | + end |
| 141 | + async show_on: "show_hello", id: "hello" do |
| 142 | + plain "hello world!" |
| 143 | + end |
| 144 | + end |
| 145 | + |
| 146 | +end |
| 147 | +``` |
| 148 | + |
| 149 | +The spec could look like this: **Note that you now have to add the `js: true` on line 3!** |
| 150 | + |
| 151 | +{% tabs %} |
| 152 | +{% tab title="spec/features/hello\_world\_spec.rb" %} |
| 153 | +```ruby |
| 154 | +require "rails_helper" |
| 155 | + |
| 156 | +describe "Some Page", type: :feature, js: true do |
| 157 | + |
| 158 | + it "should render hello world after clicking on a button" do |
| 159 | + visit some_page_path |
| 160 | + expect(page).not_to have_content("hello world!") |
| 161 | + click "click me" |
| 162 | + expect(page).to have_content("hello world!") |
| 163 | + end |
| 164 | + |
| 165 | +end |
| 166 | +``` |
| 167 | +{% endtab %} |
| 168 | +{% endtabs %} |
| 169 | + |
| 170 | +Capybara by default will wait for 2000ms before failing on an expectation. `expect(page).to have_content("hello world!")` therefore may take up to 2000ms to become truthy without breaking the spec. Following the documentation of Capybara, you can adjust the default wait time or set it individually on specific expectations. This built-in wait mechanism is especially useful when working with features requiring client-server communication, like page transitions, form or action submissions! |
| 171 | + |
| 172 | +## Testing forms and actions |
| 173 | + |
| 174 | +Imagine a `matestack_form` used for creating new `User` ActiveRecord Model instances: |
| 175 | + |
| 176 | +```ruby |
| 177 | +class SomePage < Matestack::Ui::Page |
| 178 | + |
| 179 | + def response |
| 180 | + matestack_form form_config do |
| 181 | + form_input key: :name, type: :text, label: "Name" |
| 182 | + button "submit me", type: :submit |
| 183 | + end |
| 184 | + toggle show_on: "succeeded" do |
| 185 | + plain "succeeded!" |
| 186 | + end |
| 187 | + toggle show_on: "failed" do |
| 188 | + plain "failed!" |
| 189 | + end |
| 190 | + end |
| 191 | + |
| 192 | + def form_config |
| 193 | + { |
| 194 | + for: User.new, |
| 195 | + path: users_path, |
| 196 | + method: :post, |
| 197 | + success: { emit: "succeeded" }, |
| 198 | + failure: { emit: "failed" } |
| 199 | + } |
| 200 | + end |
| 201 | + |
| 202 | +end |
| 203 | +``` |
| 204 | + |
| 205 | +The according spec might look like this: |
| 206 | + |
| 207 | +{% tabs %} |
| 208 | +{% tab title="spec/features/form\_submission\_spec.rb" %} |
| 209 | +```ruby |
| 210 | +require "rails_helper" |
| 211 | + |
| 212 | +describe "Some Page", type: :feature, js: true do |
| 213 | + |
| 214 | + it "should render hello world" do |
| 215 | + visit some_page_path |
| 216 | + fill_in "Name", with: "Foo" |
| 217 | + click "submit me" |
| 218 | + |
| 219 | + expect(page).to have_content("succeeded!") |
| 220 | + end |
| 221 | + |
| 222 | +end |
| 223 | +``` |
| 224 | +{% endtab %} |
| 225 | +{% endtabs %} |
| 226 | + |
| 227 | +If you want to test if the User model was correctly saved in the Database, you could do something like this: |
| 228 | + |
| 229 | +{% tabs %} |
| 230 | +{% tab title="spec/features/form\_submission\_spec.rb" %} |
| 231 | +```ruby |
| 232 | +describe "Some Page", type: :feature, js: true do |
| 233 | + |
| 234 | + it "should render hello world" do |
| 235 | + visit some_page_path |
| 236 | + fill_in "Name", with: "Foo" |
| 237 | + |
| 238 | + expect { |
| 239 | + click "submit me" |
| 240 | + expect(page).to have_content("succeeded!") #required to work properly! |
| 241 | + }.to change { User.count }.by(1) |
| 242 | + |
| 243 | + # from here on, we know for sure that the form was submitted |
| 244 | + expect(User.last.name).to eq "Foo" |
| 245 | + end |
| 246 | + |
| 247 | +end |
| 248 | +``` |
| 249 | +{% endtab %} |
| 250 | +{% endtabs %} |
| 251 | + |
| 252 | +{% hint style="danger" %} |
| 253 | +**Beware of the timing trap!** |
| 254 | + |
| 255 | +Without adding `expect(page).to have_content("succeeded!")` after `click "submit me"` the spec would fail. The `User.count` would be executed too early! You somehow need to use Capybara's built-in wait mechanisim in order to identify a successful asynchronous form submission. Otherwise the spec would just click the submit button and immediately expect a database state change. Unlike Capybara, plain Rspec expectations do not wait a certain amount of time before failing! Gems like [https://github.com/laserlemon/rspec-wait](https://github.com/laserlemon/rspec-wait) are trying to address this issue. In our experience, you're better of using Capybara's built-in wait mechanism like shown in the example, though. |
| 256 | +{% endhint %} |
| 257 | + |
| 258 | + Above described approaches and hints apply for actions as well! |
| 259 | + |
| 260 | +## Optional: Dockerized test setup \[WIP\] |
| 261 | + |
0 commit comments