End-to-end testing w/Rails 5+, Angular 5+, Capybara, and Selenium: Part 3 of 4

By Zach Dennis on 29 Jun 2018

In Part 2 of this series we completed the task of setting up end-to-end testing with an Angular 6 frontend and a Rails 5 API backend, from scratch. It was awesome. Today, we’re going to walkthrough the Capybara configuration we ended up with in Part 2.

Originally, this post was going to also include information on how to set up these two things:

  • Configuring our database cleaning strategy
  • Capturing the browser’s developer console output

But, this post got long enough on its own so I scaled it back to keep it focused. For this post, you’ll get the most out of this post if you’ve gone through the walkthrough in Part 2, but that’s not required.

Let’s get started.

Configuring Capybara

We stored the Capybara configuration in spec/support/capybara.rb in Part 2.

Why is this not in one of the other configuration files?

You may be wondering: Why not put this the configuration in spec/rails_helper.rb or spec/spec_helper.rb?

We-ell, I find it simpler to understand, easier to modify/delete, and requires less mental energy to grok.

What’s in the configuration file?

Here’s a quick refresher on what the file looked like at the end of Part 2:

Chunk 1: The requires

The require(s) are pretty straightforward:

  • capybara/rails loads the main Capybara library specifically for Rails applications
  • capybara_spa loads a third-party CapybaraSpa library used for helping managing single-page application servers.
  • selenium/webdriver loads up Selenium::WebDriver library

Chunk 2: Inline docs

The next chunk in the file is basic inline documentation:

I find it helpful to document the purpose of a class, module, or as in this case a configuration file. The act of writing it down, getting the intentions out of my head helps me avoid the temptation of lumping a bunch of unrelated things together (which happens when in the throes of figuring something out).

And, as part of the inline docs I try to pul up what I think might be the most useful bits for someone to glean if they just skimmed the top of this file. In case this, I thought pointing out the two different JavaScript drivers would be helpful.

Chunk 3: Setting up the FrontendServer

In this end-to-end test setup we need something running that can serve our Angular app. The Rails application is not serving JavaScript assets (if you recall it’s an API server) so the end-to-end test suite needs something capable of doing so.

Enter the instantiation of CapybaraSpa::Server::NgStaticServer:

What is CapybaraSpa::Server::NgStaticServer ? It’s a class that capable of serving a static build of an Angular application. We pass to it four pieces of information:

  • build_path: The path to where the static build of the Angular application exists.
  • http_server_bin_path: The path to the angular-http-server binary. Only required if angular-http-server cannot be found in PATH.
  • log_file: The path to the file where the angular-http-server server process output should be written to. It is optional and when not provided output will be logged to STDOUT.
  • pid_file: The path to to the file where the pid will be written for the angular-http-server child process that will be managed.

Once we have an instance of NgStaticServer all we need to do next is determine when to start and stop it. But, first, let’s look at why this is not in an RSpec configuration block.

Why NgStaticServer is not instantiated within an RSpec configuration block?

Short answer: Because we only need to instantiate it once for the entire test run.

Longer answer: The test suite needs just one way to serve an Angular application. By instantiating NgStaticServer once when the spec/support/capybara.rbfile loads we’re able to assign it a top-level constant (FrontendServer) which makes it easily accessible throughout the test suite. This doesn’t actually fire up the underlying angular-http-server child process.

The starting and stopping of the NgStaticServerhappens within an RSpec configuration block, but the instantiation we only need once. Let’s look at that next.

Configuring RSpec

A wonderful feature of RSpec is the ability to distribute its configuration. Rather than have a configuration monolith in rails_helper.rb or spec_helper.rb files we can organize configuration by responsibility. For this particular file we only need to include relevant configuration necessary to support our end-to-end tests that rely on Capybara.

Here’s the RSpec configuration for our FrontendServer:

Starting the FrontendServer

This configuration uses RSpec’s before(:each) hook to make sure that the FrontendServer is started before every example tagged with :js (e.g. examples that require JavaScript) executes. Plus, we only start the FrontendServer if it is not already started:

Why the begin/rescue block?

I ran into a situation where Capybara was swallowing errors when something went wrong when the test suite first booted up. This was occurring when the FrontendServer failed to start before Capybara finished booting up Puma to run the backend API. It would eventually fail, but rather than wait around for the failure begin/rescue block lets us fail fast, reporting the error to the user right away.

Stopping the FrontendServer

This configuration uses RSpec’s after(:suite) hook to stop the FrontendServer once the test suite is done (and only if it is currently started?).

This helps ensure that the test suite is not leaving around any zombie processes.

Note: CapybaraSpa::Server::NgStaticServer will attempt to clean up any child processes when the ruby process running the test suite exits, but this is more of a safeguard to protect against zombie processes. Having stop be explicit is more communicative of our test suite’s intent.

Now that the FrontendServer is configured to be started and stopped at the right times, let’s turn our attention to registering the drivers that will drive our browser, Google Chrome.

Registering our Chrome drivers

The next part of the file registers two Chrome drivers: :chrome and :headless_chrome.

Before they are registered we set up a few different options. First, we have some stock code for determining where the Chrome binary is:

As you can tell from the comments the chrome_bin variable can be set from two environment variables: GOOGLE_CHROME_SHIM or CHROME_BIN.

Why are there different?

GOOGLE_CHROME_SHIM comes from the google-chrome heroku buildpack. If you use Heroku’s CI services then you’ll need this.

CHROME_BIN comes from other CI environments where you install chromedriver into an unknown location. For example, when setting up chromedriver on TravisCI it’s often recommended to set the CHROME_BIN environment variable to where ever you’re installing chromedriver too.

Logging Preferences

We initialize a logging_preferences variable after the chrome_options. This is to enable capturing Chrome’s browser logs including JavaScript warnings and errors you’d see in the developer console:

You’ll see when registering the individual drivers where this is used.

Note: This enables it in the browser but it does not put the information anywhere. We’ll see later on how to capture it.

Registering the :chrome driver

The first driver registered is the :chrome driver. We can name the driver whatever we want. Capybara keeps track of all of the drivers registered and allows us to switch between drivers as needed.

The :chrome driver we register passes in the chrome_options and logging_preferences we set above and the returns a new Capybara::Selenium::Driver instance.

This was called :chrome because this runs the Chrome browser (even popping up a new Chrome window) when the test suite runs:

Having the :chrome driver can be really for debugging as you can interact with the browser.

Sometimes though, you don’t need to debug, and want the test suite to run as quickly as possible. This brings us to the :headless_chrome driver.

Registering the :headless_chrome driver

The:headless_chrome driver is near identical to the :chrome, but it takes in in additional chrome_options in order to have Chrome run headless and without gpu optimizations:

Running headless can improve the speed of the test suite. This is often the default for CI environments running an end-to-end test suite.

Configuring Capybara

The last few lines of this file are for configuring Capybara itself:

Let’s walk over each line one by one.

Capybara.app_host

Capybara.app_host should be the URL for the frontend application. Since we are running Angular we need to the domain and port that the Angular application is running on. We use FrontendServer.portto tell us what port the HTTP server that is serving up the Angular app is running on:

Capybara.always_include_port

Capybara.always_include_port is to ensure that the port is always included when visiting URLs in a test suite. Since our Angular app is running on a different port we want to always make sure the port is present:

Capybara.default_max_wait_time

Capybara.default_max_wait_time is the maximum number of seconds to wait for asynchronous processes to finish.

This may be easier to grok with a concrete example. Let’s say you’re testing an application that adds items to a list. When you first load the page the list is empty. Your test goes like this:

  • Go to the items page
  • Sees there are no items
  • Clicks the “Add Item” button
  • Fills in an item name
  • Click “Save”
  • See item is added to the list

The second to last part above (clicking “Save”) fires off an XHR call to save the item to the server. The frontend may not show the item in the list until its guaranteed to be saved on the server so it waits for the server to respond with an HTTP 201 Created response before showing the item in the list. This is where Capybara.default_max_wait_time comes into play: How long should Capybara wait to see if that item shows up?

The default is 2 seconds, but I like to set it a little bit higher. It’s important to note that this is not how long Capybara will wait. If an item shows up right away then Capybara won’t wait 10 seconds. It will only wait 10 seconds if the item never shows up. Basically, it will continually try to find the item on the page for 10 seconds. Then it gives up and raises an exception.

Capybara.javascript_driver

The Capybara.javascript_driver specifies which registered driver to use.

We registered :chrome and :headless_chrome earlier in this file and by default we’ll use the :chrome driver.

Capybara actually comes with a few basic drivers out of the box too:

  • rack_test: Not capable of executing JS, but is great and fast for testing API end-points or pages without JS.
  • selenium: Registers the barebones Selenium driver.
  • selenium_chrome: Registers a barebones Selenium driver for Chrome. Similar to what we have in this file minus the chromeOptions and loggingPrefs that we’re passing in.
  • selenium_chrome_headless:Registers a barebones Selenium driver for running headless Chrome. Similar to what we have in this file minus the chromeOptions and loggingPrefs that we’re passing in.

Capybara.server

Capybara.server is the name of the registered server to use.

Capybara comes with a few basic servers out of the box too:

  • default: Defaults to running capybara’s default server (which is :puma)
  • webrick: Uses WEbrick to run the Rails server.
  • puma: Explicitly use Puma to run the Rails server.

Capybara.server_port

Capybara.server_port is the port that the Rails application is running on:

We hard-code this to 3001 because it is going to be referenced from the Angular application’s end-to-end build configuration.

Summary

After reading this post you should feel you have a good grasp as to what’s going on in the spec/support/capybara.rb configuration file, how the configuration supports end-to-end testing with our Angular frontend, and why the settings are the way they are.

In the next post, we’re going to look at two common pieces of functionality:

  • Configuring our database cleaning strategy
  • Capturing the browser’s developer console output

Until then, happy coding!

About Mutually Human

Mutually Human is a custom software design and development consultancy specializing in mobile and web-based products and services. We help our clients design, develop and bring to market innovative products and services based on insightful research and strategy aligned with business objectives. We’ve helped Fortune 500 companies, state governments, and startups.