Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Capybara assertion support to Controller, Integration, Mailer, and View tests #43361

Open
wants to merge 3 commits into
base: main
Choose a base branch
from

Conversation

seanpdoyle
Copy link
Contributor

@seanpdoyle seanpdoyle commented Oct 2, 2021

Motivation / Background

This is a re-submission of #41291.

Capybara provides a wide-range of selectors, which
can be extended even further.

While Capybara is capable of exercising browser-based, JavaScript-enabled pages, its assertions and selectors excel in JavaScript-free static HTML environments.

HTML-only assertions make Capybara a compelling alternative to the rails-dom-testing assertions utilized by ActionController::TestCase, ActionDispatch::IntegrationTest, and ActionView::TestCase.

For example, Capybara provides a :button selector, which can be
invoked by assert_selector :button, "Button Text" or assert_button "Button Text". Additionally, Capybara's button selector supports
resolution based on ARIA attributes like aria-label and
[role="button"]. The assertions provided by rails-dom-testing do
not.

It's possible to recreate those assertion characteristics gem-side in
rails-dom-testing, internally Rails-side, or application-side.

Given the fact that capybara is already a Rails testing dependency
(through System Tests), there is an opportunity re-use that dependency across all HTML test cases.

Detail

This commit introduces framework-specific configuration values:

  • config.action_controller.html_assertions for ActionController::TestCase
  • config.action_dispatch.html_assertions for ActionDispatch::IntegrationTest
  • config.action_mailer.html_assertions for ActionMailer::TestCase
  • config.action_view.html_assertions for ActionView::TestCase

When set to :capybara, those tests include framework-scoped CapybaraAssertions modules that transitively include Capybara::Minitest::Assertions, and define the required #page method to parse the HTML into a Capybara::Node::Simple instance.

When set to :rails_dom_testing, those tests include a framework-scoped RailsDomTestingAssetrions module to preserve the existing behavior.

They all default to :rails_dom_testing.

Additional Information

I've forked eileencodes/integration_performance_test and updated it to work with this branch:

eileencodes/integration_performance_test@master...seanpdoyle:rails/action-dispatch-integration-capybara

The results show that asserting with Capybara::Minitest::Assertions is roughly 1.11x slower than Rails::Dom::Testing::Assertions

Calculating -------------------------------------
ActionController::TestCase: rails-dom-testing
                       139.000  i/100ms
ActionController::TestCase: capybara/minitest
                       132.000  i/100ms
-------------------------------------------------
ActionController::TestCase: rails-dom-testing
                          1.479k (± 1.5%) i/s -      7.506k
ActionController::TestCase: capybara/minitest
                          1.328k (± 0.8%) i/s -      6.732k

Comparison:
ActionController::TestCase: rails-dom-testing:     1479.1 i/s
ActionController::TestCase: capybara/minitest:     1327.6 i/s - 1.11x slower

Calculating -------------------------------------
ActionDispatch::IntegrationTest: rails-dom-testing
                       121.000  i/100ms
ActionDispatch::IntegrationTestINDEX: capybara/minitest
                       112.000  i/100ms
-------------------------------------------------
ActionDispatch::IntegrationTest: rails-dom-testing
                          1.237k (± 0.7%) i/s -      6.292k
ActionDispatch::IntegrationTestINDEX: capybara/minitest
                          1.117k (± 1.1%) i/s -      5.600k

Comparison:
ActionDispatch::IntegrationTest: rails-dom-testing:     1237.2 i/s
ActionDispatch::IntegrationTestINDEX: capybara/minitest:     1117.0 i/s - 1.11x slower

Run options: --seed 59692

# Running:

....

Finished in 0.004782s, 836.4701 runs/s, 2509.4103 assertions/s.
4 runs, 12 assertions, 0 failures, 0 errors, 0 skips

Checklist

Before submitting the PR make sure the following are checked:

  • This Pull Request is related to one change. Changes that are unrelated should be opened in separate PRs.
  • Commit message has a detailed description of what changed and why. If this PR fixes a related issue include it in the commit message. Ex: [Fix #issue-number]
  • Tests are added or updated if you fix a bug or add a feature.
  • CHANGELOG files are updated for the changed libraries if there is a behavior change or additional feature. Minor bug fixes and documentation changes should not be included.

@rails-bot
Copy link

rails-bot bot commented Jan 12, 2022

This pull request has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs.
Thank you for your contributions.

@rails-bot rails-bot bot added the stale label Jan 12, 2022
@rails-bot rails-bot bot closed this Jan 20, 2022
@nimmolo
Copy link

nimmolo commented Jul 9, 2022

Hey, just want to say this looks like a desirable PR.
Can this be re-opened please?

@nimmolo
Copy link

nimmolo commented Sep 3, 2022

@seanpdoyle maybe the lack of context is the reason for the lack of further discussion on this PR. It's such a useful implementation... and it seems to me you've made requested modifications and done the benchmarking.

Would it make sense to abandon this newer PR, and consolidate these changes into the older PR branch, so reviewers and core committers have the reminders of the work and review that has already gone into this?

@cpjmcquillan
Copy link
Contributor

@seanpdoyle awesome work, +1-ing that it would be great if this was reconsidered.

Oh, and thanks again for all your fantastic contributions to the community!

@p8 p8 reopened this Sep 6, 2022
@rails-bot rails-bot bot removed the stale label Sep 6, 2022
@seanpdoyle seanpdoyle force-pushed the action-dispatch-integration-capybara branch 2 times, most recently from 909ac38 to a1ce763 Compare September 6, 2022 16:10
@rails-bot rails-bot bot added the docs label Sep 6, 2022
@seanpdoyle seanpdoyle force-pushed the action-dispatch-integration-capybara branch from a1ce763 to 3599b88 Compare September 6, 2022 16:13
@nimmolo
Copy link

nimmolo commented Sep 8, 2022

thanks @p8, @cpjmcquillan and @seanpdoyle !

@seanpdoyle seanpdoyle force-pushed the action-dispatch-integration-capybara branch 2 times, most recently from 6a3b28e to 10d5797 Compare September 10, 2022 21:47
@seanpdoyle
Copy link
Contributor Author

Unfortunately, #41291 was closed due to inactivity, and as a result, the context for this discussion must be split between two pull requests.

Due to that split, I need to ping @eileencodes @rafaelfranca @zzak, since you were all involved in the original discussion.

Given the concerns about the performance impacts of the original changeset, I've re-structured the proposal to a point where it's entirely opt-in.

Is this approach more viable than the original?

@rafaelfranca
Copy link
Member

I think so, but I also think is more confusing. Why would someone chose this new list of assertions over the existing ones? Rails is opinionated, and having two set of assertions for the same kind of tests seems that we don't have an opinion in the matter.

I still don't understand why we wouldn't improve rails-dom-testing. If the capybara assertions were not slower I'd even say we could just stop using rails-dom-testing, but that doesn't seems to be the case since those capybara assertions are almost 2 times slower in the worse case.

@seanpdoyle
Copy link
Contributor Author

@rafaelfranca thank you for jumping back into this conversation! I'm sorry for the long pause and the change in venue.

Why would someone chose this new list of assertions over the existing ones?

Capybara's built-in selectors are more robust and flexible than what rails-dom-testing provides. Built-in selectors like :button or :field are more sophisticated than their String selector rails-dom-testing counterparts. For example, Capybara can find a <button> element based on its text content, [type] value, and presence or absence of the [disabled] attribute:

# <button>Submit</button>

assert_button "Submit"
assert page.find(:button, "Submit")

# <button type="reset">Reset</button>

assert_button type: "reset"
assert page.find(:button, type: "reset")

# <button disabled>Submit</button>
assert_no_button "Submit"
page.find(:button, "Submit") #=> raises!

assert_button "Submit", disabled: true
assert page.find(:button, disabled: true)

# <button>Submit</button>
# <button type="button">Cancel</button>

assert_button { _1.text.include?("Submit") && _1["type"] != "button" }

rails-dom-testing lacks built-in abstractions, and instead relies on String selectors. This leaves the burden of responsibility with applications to develop their own abstractions and helpers. Without those abstractions, assertions about <button> elements might resemble:

# <button>Submit</button>

assert_select "button", "Submit"

# <button type="reset">Reset</button>

assert_select 'button[type="reset"]'

# <button disabled>Submit</button>
assert_no_select "button:not([disabled])", "Submit"

assert_select "button[disabled]", "Submit"

Some selectors can be mapped directly one-to-one from Capybara to rails-dom-testing. However, it gets tricky for elements that derive their "text" from somewhere other than the element's .textContent. Take, for example, an <input type="submit"> or <input type="button">. Capybara's built-in selectors are capable of distinguishing between the two:

# <input type="submit" value="Submit">
# <input type="button" value="Submit">
# <button>Submit</button>
# <button type="button">Submit</button>

assert_button "Submit", count: 4
assert_button "Submit", type: "button", count: 2

Calls to assert_select, however, must account for the textContent-[value] differences and the input-button differences. While it's true that applications or rails-dom-testing itself could come up with abstractions to paper over those differences, that effort would duplicate what Capybara already provides.

Another feature of Capybara's selectors that would be challenging to recreate in rails-dom-testing is its support for locating :field (and :fillable_field, and :file) based on corresponding <label> text or [aria-label] value.

# <label for="a_text_field">A text field</label>
# <input id="a_text_field" value="an initial value">

assert_field "A text field", with: "an initial value"

The accessibility benefits alone feel valuable enough to warrant a migration. Personally, I find support for asserting that form controls are properly labelled without the need for JavaScript-enabled System Tests very compelling.

having two set of assertions for the same kind of tests seems that we don't have an opinion in the matter.

I think we're in agreement here. My perspective is that since most applications already depend on Capybara for driving their System Tests, integrating those predictable and robust selectors into their Rack Test driven integration tests could halve their maintenance burden. Choosing Capybara over rails-dom-testing means they only need to create the abstraction once, and use it anywhere their test harness interacts with HTML. Some of those applications might already have their own suite of domain-specific bespoke Capybara selectors (or depend on third-party selectors like those provided by capybara_accessible_selectors). Should they also maintain a collection of bespoke rails-dom-testing selectors, helpers, and abstractions for their Integration test suite?

If the capybara assertions were not slower I'd even say we could just stop using rails-dom-testing, but that doesn't seems to be the case since those capybara assertions are almost 2 times slower in the worse case.

This is an unfortunate concern. My original proposal considered replacing rails-dom-testing, but felt that would be too disruptive. I wonder what kind of effort it would require to make Capybara a more performant option. From my perspective, I consider optimizing Capybara a more cost-effective use of time and effort than reverse engineering some of Capybara's features back into rails-dom-testing.

@rafaelfranca
Copy link
Member

To me, given all options capybara helpers provide over our own helpers I can't see why Rails should not provide that as default, other than the performance concerns. We spent a considerable amount of time trying to make ActionDispatch::IntegrationTest as fast as ActionController::TestCase to open the avenue of deprecating/removing the latter, so making capybara helpers the default in IntegrationTest would goes against that.

But, if we can make Capybara helpers as fast as the ones we have today, I don't see a reason why we should not do that. I totally agree that if not too hard, we should spend time optimizing Capybara instead of reimplementing it.

@eileencodes
Copy link
Member

My perspective is that since most applications already depend on Capybara for driving their System Tests

I'm not sure this is a true statement. I can see why it seems that way, but any apps upgrading wouldn't be forced to add Capybara unless they wanted system tests and any application that's generated as just an API skips system tests. It's also pretty easy to remove. So while we can say a fair amount of applications already depend on Capybara it's much less of a requirement than replacing rails-dom-testing with Capybara would be. We'd be making a hard requirement on Capybara for default integration testing, whereas right now it's only a hard requirement for system testing.

But, if we can make Capybara helpers as fast as the ones we have today, I don't see a reason why we should not do that.

I think this is a big concern of mine as well, since I worked on making integration tests faster so they were in line with functional/controller tests. I wouldn't be supportive of undoing that work.

The other side of it is that currently we control rails-dom-testing but we have no control over how and when Capybara fixes bugs or adds features. I'm sure that it's well maintained but it is a complex codebase and would make it harder for the core team to have influence into how it affects the framework and testing environments. There is a non-zero chance it would increase the maintenance burden for us. This is fine for system testing because there aren't many other alternatives we could plug and play into Rails, and system testing isn't your default go to when testing the majority of your framework (mainly because system testing is pretty slow and should be mainly used for testing how Rails and JS interact).

I can tell you from first hand experience that since I'm not that familiar with Capybara it's been difficult for me to decide when to merge changes into Rails that "fix" system tests. I'm often relying on the PR author to have done due diligence into the right fix because I don't know Capybara well enough.

TL;DR:

My main concerns for moving to Capybara from rails-dom-testing are maintenance burden and performance. Improving Capybara perf gets rid of one my concerns.

@seanpdoyle
Copy link
Contributor Author

Thank you two for your perspectives. While I'm personally interested in exploring ways to optimize Capybara, I'm curious about the viability of this Pull Request in its current form.

The Capybara assertions are opt-in, since the ActionDispatch::Assertions::CapybaraAssertions module isn't mixed into ActionDispatch::IntegrationTest by default.

This changeset isn't replacing or deprecating rails-dom-testing. It also isn't introducing a Capybara dependency where one did not exist before (right?). Does the file's presence mean that it's autoloaded in a way that carries along the Capybara dependency from the inline gem "capybara" line?

This diff is the least disruptive version of these changes I could imagine. If the team isn't interested in accepting them, could we instead determine a more public interface so that a third-party gem could extend ActionDispatch::IntegrationTest without overriding the very private _mock_session method?

@rafaelfranca
Copy link
Member

could we instead determine a more public interface so that a third-party gem could extend ActionDispatch::IntegrationTest without overriding the very private _mock_session method?

I'm up for that.

While I can see this PR as a good compromise it goes against the opinionated nature of Rails. We are including 2 solutions inside the framework, when we usually are very opinionated on what should be used. An API to allow people to build on top of our system gives people to flexibility to chose, while keeping our opinion.

@rails-bot rails-bot bot added the railties label Nov 18, 2023
@seanpdoyle seanpdoyle force-pushed the action-dispatch-integration-capybara branch 5 times, most recently from cda3a52 to cbc01c7 Compare November 18, 2023 13:40
@seanpdoyle seanpdoyle force-pushed the action-dispatch-integration-capybara branch 5 times, most recently from fa8c2a6 to c571a1f Compare December 2, 2023 01:52
@seanpdoyle
Copy link
Contributor Author

@rafaelfranca @eileencodes I've reconsidered the initial proposal implemented by e428beb.

In its place, c571a1f introduces a config.FRAMEWORK.assertions configuration value to control the assertions used by ActionController::TestCase, ActionDispatch::IntegrationTest, and ActionView::TestCase.

I've updated the PR's description to encompass the changes, and I've included changes to Guides documentation.

Since performance impacts were a concern, I've included some rough benchmarks in the PR description. Swapping :capybara for :rails_dom_testing is 1.11x slower.

Is this approach an improvement on the initial proposal?

@seanpdoyle seanpdoyle force-pushed the action-dispatch-integration-capybara branch from c571a1f to f79147c Compare December 2, 2023 13:39
@seanpdoyle seanpdoyle force-pushed the action-dispatch-integration-capybara branch 3 times, most recently from 819cdad to 0566707 Compare January 5, 2024 02:39
@seanpdoyle seanpdoyle force-pushed the action-dispatch-integration-capybara branch 2 times, most recently from ce1ba4a to f84055a Compare January 13, 2024 04:31
@seanpdoyle seanpdoyle force-pushed the action-dispatch-integration-capybara branch from f84055a to 2055f66 Compare January 13, 2024 04:32
@seanpdoyle seanpdoyle changed the title Add Capybara assertion support to Controller, Integration, and View tests Add Capybara assertion support to Controller, Integration, Mailer, and View tests Jan 13, 2024
@seanpdoyle seanpdoyle force-pushed the action-dispatch-integration-capybara branch 3 times, most recently from 9f061c7 to 6cd5267 Compare January 14, 2024 20:22
@seanpdoyle seanpdoyle force-pushed the action-dispatch-integration-capybara branch from 6cd5267 to f208914 Compare January 30, 2024 03:15
@seanpdoyle seanpdoyle force-pushed the action-dispatch-integration-capybara branch from f208914 to 49991ec Compare March 2, 2024 15:35
Also introduce `#assert_text_part` and `#assert_html_part` convenience
methods:

```ruby
test "asserts text parts" do
  mail = MyMailer.welcome("Hello, world")

  assert_part mail, :text do |part|
    assert_includes part.body.raw_source, "Hello, world"
  end
  assert_text_part mail do |text|
    assert_includes text, "Hello, world"
  end
end

test "asserts html parts" do
  mail = MyMailer.welcome("Hello, world")

  assert_part mail, :html do |part|
    assert_includes part.body.raw_source, "Hello, world"
  end
  assert_html_part mail do |html|
    assert_select html, "p", "Hello, world"
  end
end
```
This is a re-submission of rails#41291.

Both `ActionDispatch::IntegrationTest` and Capybara in `:rack_test` mode
are capable of exercising Rails applications that serve HTML over HTTP.

Capybara itself provides a [wide-range of selectors][selectors], which
can be [extended even further][capybara_accessible_selectors]. While
Capybara's JavaScript far exceeds `ActionDispatch::IntegrationTest`'
HTML-only support, their overlapping capabilities makes Capybara an
interesting candidate to supplant `ActionDispatch::IntegrationTest`'s
assertions.

For example, Capybara provides a `:button` selector, which can be
invoked by `assert_selector :button, "Button Text"` or `assert_button
"Button Text"`. Additionally, Capybara's `button` selector supports
resolution based on ARIA attributes like `aria-label` and
`[role="button"]`. The assertions provided by `rails-dom-testing` do
not.

It's possible to recreate those assertion characteristics in
`rails-dom-testing`, Rails itself, or a consumer application. However,
given the fact that `capybara` is already a Rails testing dependency
(through System Test support), and the fact that they're both capable of
coordinating with `Rack::Test`, there is an opportunity to unify them.

[selectors]: https://github.com/teamcapybara/capybara/tree/84acc29d5ff807507fe57aafcf7f9b2acdb89fe2/lib/capybara/selector/definition
[capybara_accessible_selectors]: https://github.com/citizensadvice/capybara_accessible_selectors/tree/d61971c609e3b019df6dc0ea0c9ce11433f3d0f7#documentation

Action Dispatch: Use `build_rack_mock_session` in test
---

This commit introduces the
`ActionDispatch::Assertions::CapybaraAssertions` module to override the
`ActionDispatch::Integration::Session#_mock_session` implementation by
substituting the `Rack::MockSession.new` call with a memoized delgation
to the an already constructed `Rack::MockSession` instance created by
the `RackTest`-driven `Capybara::Session`.

This commit includes a new test case that extends the `Session` instance
to integrate with `Capybara`.``

Alternatives
---

This is currently possible, without any implementation changes.
Applications could create their own versions of
`ActionDispatch::Assertions::CapybaraAssertions`by overriding
`_mock_session`. That feels like a blatant violation of the public API.
Having said that, if a third-party package were to provide this kind of
Capybara integration, it would have to re-open the class and target the
private `_mock_session` method for overriding.

Similarly, [open_session][]'s documentation mentions the pattern of
using `#extend` on the block's `Session` instance. However, I'm not sure
if there's a way to promote the block argument to serve globally as the
overridded `#integration_session` that methods are delegated to.

[Rack::Test::Methods]: https://github.com/rack/rack-test/blob/v1.1.0/lib/rack/test/methods.rb#L29-L31
[Capybara::RackTest::Browser]: https://github.com/teamcapybara/capybara/blob/3.35.3/lib/capybara/rack_test/browser.rb#L128-L131
[open_session]: https://github.com/rails/rails/blob/main/actionpack/lib/action_dispatch/testing/integration.rb#L377-L386
…d View tests

This commit introduces framework-specific configuration values:

* `config.action_controller.assertions` for `ActionController::TestCase`
* `config.action_dispatch.assertions` for `ActionDispatch::IntegrationTest`
* `config.action_mailer.assertions` for `ActionMailer::TestCase`
* `config.action_view.assertions` for `ActionView::TestCase`

When set to `:capybara`, those tests include framework-scoped
`CapybaraAssertions` modules that transitively include
`Capybara::Minitest::Assertions`, and define the required `#page` method
to parse the HTML into a `Capybara::Node::Simple` instance.

When set to `:rails_dom_testing`, those tests include a framework-scoped
`RailsDomTestingAssetrions` module to preserve the existing behavior.

They all default to `:rails_dom_testing`.
@seanpdoyle seanpdoyle force-pushed the action-dispatch-integration-capybara branch from 49991ec to c4be01c Compare April 12, 2024 20:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

6 participants