HTTPS integration tests with Rails and Capybara
At Doctolib, we strongly believe in testing. The test suite for our monolith is currently backed up by more than 12,000 tests, and growing everyday. These 12,000 tests run on every commit that gets published.
We also strongly believe in security, one of our 5 main engineering values. And we recently made the switch to have our integration tests run in HTTPS🔒.
Why do that?
Like all developers, we need to have our local development environment running on our own machines to build new amazing features, identify and fix pesky little bugs, and improve performance everyday. Some Doctolib features such as video consultation require running the application in HTTPS. Also, some of us simply prefer running their local application in HTTPS, since this is how it will eventually be served to the world.
However, this proved to be a problem for us. Our application is backed by a modern Rails stack and Webpacker powers our asset pipeline, so to run our Rails app, we launch both:
- A Rails server
- A webpack server (through webpacker)
Both of them work in HTTPS, which makes for a great development setup.
When we run our integration test suite, we still need webpack to serve the assets (or get the assets pre-built, but this is out of the scope of this post). Alas, our integration suite was not configured to run with a HTTPS webpack server.
Therefore, when working on the application, all developers running their local development environment in HTTPS were trapped in a very inefficient loop:
- when testing the changes in the app…
- ➡ run webpack in HTTPS🔒
- when writing and running integration tests…
- ➡ ️stop webpack
- ➡ ️restart webpack in HTTP mode
- ➡ ️run the integration tests
And back to HTTPS to test in the app, then to HTTP for test, etc. You get the idea. Each time, tens of seconds lost, tens of times each day, for tens of developers. And in the end, a frustrating developer experience. For all those reasons, we needed to improve this.
Which parts are involved?
This is far from obvious when you run therails test
command, but a lot is going on under the hood, especially for those integration tests. Knowing all those pieces is important before diving any further into our problem. Let’s make a quick round-up of the many pieces in play:
Capybara
Capybara is the stepping stone of our integration test suite, as is the case with many Ruby and Rails web projects. It provides a user-friendly syntax to simulate a user interaction with an application and gracefully handles DOM lookup, executing actions and dealing with asynchronous behaviors.
Selenium & webdrivers
Selenium is an open-source automation testing tool, particularly used to automate web applications testing suites. It accepts commands and translates them into actual interactions with specific browsers through drivers. Those drivers are themselves gracefully handled by the webdrivers gem.
Thin
For an integration test suite to interact with a web application, this application first needs to be alive and kicking! And for this, it needs a server. We’ve had some problems running our test suite on Puma (which is very unfortunate because Puma has a built-in option for SSL). Thin is our web server of choice for our integration test suite, and one of the most-well known ones in the Ruby ecosystem.
Webpack (via webpacker)
Since our web application is running during an integration test, it also needs frontend assets to be available. Those assets are built and served by webpack through the Webpacker gem.
Back to business: how do we set this thing up?
OK, so what is going on here? When we run the rails test
command for integration testing, our application is started, running on a Thin web server. Webpack also runs so that the application can access its assets. The Rails application builds all the assets paths expecting them to be delivered in HTTP, and therefore, can’t reach the actual assets when they are served in HTTPS. This is what we need to address.
Configuring Capybara app server
It seems our first step must involve the Thin web server configuration somehow, and the way Capybara asks it to run the application.
In the Capybara source code, we can find a way to register additional servers using the Capybara.register_server
method. Let’s start by adding a new Thin server, and call it thin_ssl
.
A good start! But how do we configure this server to have SSL capabilities? That’s the real issue here. We need to tell :thin_ssl
to run in HTTPS, using our local self-signed SSL certificates:
Those options inform Thin that it has to run in SSL mode, with the given private key and certificate files. Now, we only need to inform Capybara to use this new :thin_ssl
server rather than the regular one:
…and monkey-patching!
At this point, we would think our application server is ready to go, but actually it won’t answer at all and just times out.
After searching the depth of the internet about this, it seems that right now, Capybara needs to be monkey-patched to work properly, otherwise the Capybara server responsiveness check hangs, and the server is deemed unresponsive, until a timeout occurs… which is not what we want, like, at all.
Here’s how to bypass this:
Registering a new Capybara driver
So far, we’ve instructed Capybara to boot our Rails application using Thin, with the appropriate SSL configuration options, and monkey-patching that prevents it from timing out.
However, as we’ll be using a self-signed certificate on localhost, our browser needs a few more arguments to behave. For this, we need to register a new Capybara driver, which will be used to access the application in a browser using HTTPS:
So from now on, Capybara uses an HTTPS server to run the application, and has a driver that tries to reach the application on HTTPS, ignoring potential warnings related to a self-signed certificate.
Looks like we’re good 🎉🎉🎉
Integration tests are the new controller tests
Well… almost good. Indeed, according to the Rails 6 guidelines, ActionController::TestCase
has been deprecated and controller tests should now be inheriting from the ActionDispatch::IntegrationTest
class.
This causes a problem: HTTP requests explicitly made from the controller tests were not sent in HTTPS, and therefore got redirected every time to their HTTPS counterpart, making our controller/integration assertions on statuses and responses fail.
The fix isn’t the most elegant but is quite simple: force the use of SSL for manual requests in ActionDispatch::IntegrationTest
:
Yippee kai!
We got it! Now, we can run our local development environment in HTTPS and seamlessly write and run integration tests, then get back to the application. And the icing on the cake, our controller tests which inherit from ActionDispatch::IntegrationTest
now also use HTTPS and behave as intended. Mission accomplished! ✅
So you love capybaras?
If you love capybaras (who doesn’t?) and integration testing, want to get a glimpse of shaping the future of healthcare, or are interested in receiving ruby, rails, reactjs and other news every Monday, then you should subscribe to the Docto Tech Life newsletter 🚀, curated by the Doctolib engineering team!