Unblock Rails UI Testing with Cypress

Everybody knows that Cypress is the best way to test your UI. Actually, that’s probably not true yet as Cypress went into public beta in October… but soon everybody will know. Anyway, Cypress is a great way to test your Rails UI (or any web UI for that matter). Cypress is especially effective when your Rails view has complex JavaScript that make Capybara tests cumbersome or impossible.

We’ve become so accustomed to dealing with or ignoring Selenium/PhantomJS issues that we’ve just come to accept them. There are blog posts and articles that advocate Selenium/PhantomJS but then say things like “intermittent PhantomJS issues.” I mean, I love Capybara with its page DSL and methods like: visit, click_on, fill_in, choose, check, select, page.has_*, expect, find_field|link|button, and within. But as soon as I start to test JS-heavy code, things become problematic.

Does this sound familiar to you: “Hmmm… how do I test this? Let’s try this…” Try, fail, Google, and repeat. Try, fail, Google, and repeat. Then you just give up on Capybara for that feature and set `:js => false`.

The issue (as Kamil Ogorek exposed in Integration Tests Can Be Fun) is that Selenium/PhantomJS are black boxes. Your Ruby test asks Selenium/PhantomJS to invoke a user UI action, then Selenium/PhantomJS then asks the browser. If you don’t have timing issues (which all tests seem to have intermittently) the browser responds to Selenium/PhantomJS which responds back to your test.

Your test code has no knowledge of the browser’s lifecycle. Cypress lives in the browser. It knows about states and event loops and application code. It doesn’t have the timeout issues prevalent with Selenium/PhantomJS based UI testing.

Don’t Trust Me

At this point you may be calling “Bullshit.” Well, click here to see a video of a sample test that works against Cypress’s own web site. Better yet, take a minute, install Cypress, and open its desktop application:

$ npm install cypress
$ cypress open

In the Cypress UI, add an application by clicking on the Add Project link:

Add Project

Traverse to a working or temporary directory. Then click on the newly added link that has the same name as the folder you selected. You’ll get the following popup:

Help Getting Started

Click “OK, got it!” then click on the example_spec.js link (that you can see behind the popup above.) And bam!, you are testing:

The Kitchen Sink Test

The sample kitchen sink test has examples for more commands than you might ever need.

Look at the runtime UI. That’s Chrome! Your tests ran in Chrome! (I apologize for the overuse of exclamation points but…. Wow! This is cool stuff!). Anyway, open up Chrome Inspector and you can find the application’s source as well as the Cypress infrastructure code.

Now do this for me: With Chrome Inspection open in the Cypress Desktop app, edit your copy of example_spec.js.

Before doing anything else, note that the DSL syntax is very RSpec-like. Cypress was built on top of Mocha, a very heavily used JavaScript unit testing framework. Now add `debugger` to just before the first `should` statement and rerun the test.

Oops, sorry, Cypress already sensed the change and reran the test (like the guard-rspec Ruby gem.) And Bam! You’re in debug mode inside your tests in the browser:

Debugging Cypress Tests

You can go to the console, look at variables, checkout network requests and responses, whatever. What I do all the time is: 1) use debugger to stop the test, 2) review the state of page variables, 3) test a solution in the Chrome Console, 4) change my test (removing the debugger statement) and it reruns automatically. I’ve found that the development cycle for writing Cypress tests is very fast.

“Yeah, but I’m not that good with JavaScript” you say. Know that the JavaScript required for writing tests is very simple. The hard part is getting comfortable with the myriad of available methods. But, if you are like me, you’ll have a short list of go-to methods that allow you to automate some powerful tests.

Cypress Promise to Tests

Cypress based tests do not have the timing issues of Selenium/Capybara because, besides the fact that it runs in the browser, Cypress places all the “command” method calls in a stack of JavaScript ES6 promises. That means that click this, Ajax that, should-equal the other thing series of tests commands only happen after the prior command finished. In truth they will timeout after reasonable and configurable defaults but you can override those global timeout values on a per command basis. This promises-based strategy removes the need for wait or sleep blocks as waiting logic is build into Cypress.

That said, you may still may have timeout issues due to slow responses to Ajax requests. Know that Cypress has a very simple strategy to wait for Ajax calls to complete. And Cypress makes it crazy easy to stub responses (more on that later) so your tests run blazingly fast. Check out my VIM screenshot with a Cypress JSON fixture on the top left (ignoring NERDTree,) a test on the top right, and, on the bottom, setup code that defines browser routes that, when called, will automatically use a fixture and a stub.

Fixtures and Stubs

Expect a subsequent blog post on building Cypress stubs and fixtures for Rails apps.

Selling Cypress

The JavaScript Angular, Node.js, etc. crowd is taking to Cypress like bees to a honey pot. The large group of folks that were on the private beta (which was very easy to get on) understood immediately that Cypress was great for end-to-end testing and feature tests. They were already familiar with other JavaScript testing frameworks. I’m not in that cool crowd. I’m in the server-side group. But I’ve written Cypress tests for C#, Python, and Rails based server side applications. Some of them had Angular and Backbone front-ends and some just had prototypical proliferation of untested JavaScript. It doesn’t matter what language your application is written in. If it has a browser interface — even if there’s no JavaScript — Cypress makes it easy to verify behavior

I’ve written Cypress tests for Rails applications that had few or no tests. One was a very successful Rails application that made its creator a millionaire. I was brought in to refactor the spaghetti infected Ruby code — some methods of which had over a thousand lines of code. So, yeah, they were experiencing some flakiness when deploying production changes. For that application, writing Cypress tests was the quickest way to verifiy existing behavior and to gain confidence in new code. In a few weeks, the Cypress test suite was quite comprehensive, taking 10 minutes to run in Circle CI (that’s right, Cypress integrates well with your favorite Continuous Integration tool.) I also configured the Ruby Coverband gem to track the lines of Ruby code touched by the test suite. The Coverband report made my client even more comfortable being an early adopter of Cypress.

Because Cypress runs as an external browser-based JavaScript client to your Rails application, it doesn’t have the control RSpec has to wrap tests in transactions. To setup and tear-down model objects I created a controller that had routes callable by my Cypress tests to create and destroy nested objects via FactoryGirl. Expect another blog post that provides more detail on that strategy.

When would a Rails developer use Cypress? When what they are working on has a complex UI that has proven to be flakey. Or when it has JavaScript. Wait… That’s all Rails applications. I’ve just scratched the surface of Cypress features. Expect more blog posts on Rails-specific Cypress and be sure to hit us up with questions.

Here are a few useful links. Note that Cypress was builT on top of Mocha, Chai, and Sinon:

The Ultimate Unit Testing Cheat-sheet For Mocha, Chai and Sinon


Don Denoncourt

Thank you, Frederik, for letting me know about your Rails gem. I will check it out.

Gleb Bahmutov

Loved your blog post, Don, honestly, entire Cypress (small) team loved it.
Thank you and happy testing!

PS: “like bees to honey pot” is a magnificent line 🙂


Leave a Reply

Your email address will not be published. Required fields are marked *