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 mainCapybara
library specifically for Rails applicationscapybara_spa
loads a third-partyCapybaraSpa
library used for helping managing single-page application servers.selenium/webdriver
loads upSelenium::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 ifangular-http-server
cannot be found inPATH
. - 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 toSTDOUT
. - pid_file: The path to to the file where the
pid
will be written for theangular-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.rb
file 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 NgStaticServer
happens 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’sbefore(: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
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.port
to 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 thechromeOptions
andloggingPrefs
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 thechromeOptions
andloggingPrefs
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
: UsesWEbrick
to run the Rails server.puma
: Explicitly usePuma
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!