Learning Integration Testing
Let’s learn Integration Testing. Integration tests are critically important because they exercise your application just like a real user. Therefore, depending on the full stack from your models up through your controllers, helpers, view templates, web server, database, and middleware.
Integration tests can be brittle if they know too much about how those components work. Proper integration tests use your application as a black box. They should know as little as possible about what happens under the hood, they’re just there to interact with the interface and observe the results.
rack-test: Integration Testing
First, a little background.
Integration tests in Rails applications work by simulating a user’s experience through the HTML interface. You load up your Rails application, and your integration test harness (such as Capybara) simulates a user clicking around on the site.
Without JavaScript, Capybara uses rack-test as a driver by default and looks something like this:
- Firstly, You trigger user action by invoking one of Capybara’s DSL methods, such as visit.
- Secondly, Capybara tells its driver (rack-test) to load the requested URL.
- Thirdly, The Rack response is parsed by rack-test and saved as the current page.
Selenium, capybara-webkit, and Poltergeist: Integration Testing
JavaScript drivers work a little differently. Because browser tools like WebKit are difficult to load inside of a Ruby process, these drivers boot up an external process which can interact with the browser engine. Because the external process doesn’t have access to the in-memory instance of your Rails application, they must make actual HTTP requests.
Interactions performed using Capybara’s JavaScript drivers look something like this:
- You trigger user action by invoking one of Capybara’s DSL methods, such as visit.
- Capybara tells its driver (such as capybara-webkit) to load the requested URL.
- The driver starts its external process to hold the browser engine.
- In order to serve actual HTTP requests, Capybara boots a new instance of your Rails application in a background thread.
Capybara to the Rescue: Integration Testing
Much of Capybara’s source code is dedicated to battling this asynchronous problem. Capybara is smart enough to understand that a page may not have loaded by the time you try to interact with it. A typical interaction might look like this:
Test Thread | Application Thread |
Your test invokes visit. | Waiting for a request. |
Capybara tells the driver to load the page. | Waiting for a request. |
The driver performs a request. | Waiting for a request. |
Your test invokes click_link. | Your application receives the request. |
Capybara looks for the link on the page, but it isn’t there. | Your application sends a response. |
Capybara tries to find the element again, but it’s not there. | The driver receives the response. |
Capybara successfully finds the element from the response. | Waiting for a request. |
As you can see, Capybara handles this interaction gracefully, even though the test starts looking for a link to click on before the page has finished loading.
However, if Capybara handles these asynchronous issues for you, why is it so easy to write flapping tests with Capybara, where sometimes the tests pass and sometimes they fail?
There are a few tricks to properly using the Capybara API so as to minimize the number of possible race conditions.
Find the first matching element
Bad:
first(“.active”).click
If there isn’t an .active element on the page yet, first will return nil and the click will fail.
Good:
# If you want to make sure there’s exactly one
find(“.active”).click
# If you just want the first element
find(“.active”, match: :first).click
Capybara will wait for the element to appear before trying to click. Note that match: :first is more brittle, because it will silently click on a different element if you introduce new elements which match.
Interact with all matching elements
Bad:
all(“.active”).each(&:click)
If there are no matching elements yet, an empty array will be returned, and no elements will be affected.
Good:
find(“.active”, match: :first)
all(“.active”).each(&:click)
Capybara will wait for the first matching element before trying to click on the rest.
Directly interacting with JavaScript
Bad:
execute_script(“$(‘.active’).focus()”)
JavaScript expressions may be evaluated before the action is complete, and the wrong element or no element may be affected.
Good:
find(“.active”)
execute_script(“$(‘.active’).focus()”)
Checking a field’s value
Bad:
expect(find_field(“Username”).value).to eq(“Joe”)
Capybara will wait for the matching element and then immediately return its value. If the value changes from a page load or Ajax request, it will be too late.
Good:
expect(page).to have_field(“Username”, with: “Joe”)
Capybara will wait for a matching element and then wait until its value matches, up to two seconds.
Checking an element’s attribute
Bad:
expect(find(“.user”)[“data-name”]).to eq(“Joe”)
Capybara will wait for the matching element and then immediately return the requested attribute.
Good:
expect(page).to have_css(“.user[data-name=’Joe’]”)
Capybara will wait for the element to appear and have the correct attribute.
Looking for matching CSS
Bad:
it “doesn’t have an active class name” do
expect(has_active_class).to be_false
end
def has_active_class
has_css?(“.active”)
end
Capybara will immediately return true if the element hasn’t been removed from the page yet, causing the test to fail. It will also wait two seconds before returning false, meaning the test will be slow when it passes.
Good:
it “doesn’t have an active class name” do
expect(page).not_to have_active_class
end
def have_active_class
have_css(“.active”)
end
Capybara will wait up to two seconds for the element to disappear before failing, and will pass immediately when the element isn’t on the page as expected.
Make your resume stand out and become a Certified Capybara Testing Professional. Try free practice tests here!
A great career is just a certification away. So, practice and validate your skills to become a Certified Capybara Testing Professional.