Photo by Michael Förtsch on Unsplash

Six Steps to Getting Started with Cypress and Ruby on Rails

Leavetrack has until the last few weeks been an entirely traditional web application.

By traditional, I mean all forms are submitted and followed up with a redirect. This lends itself to effective testing through Rails' own framework. I use the Rails defaults for my tests so when testing the creation of an absence request, I could do something like the following:

test "creating absence with valid params redirects to index page"
  post :create, params: { absence: FactoryBot.attributes_for(:valid_absence) }
  assert_redirected_to absences_url
end

There would be a few flavours of this covering off items such as ensuring that the flash is set properly and testing invalid submissions but on the whole it was possible to test the functionality using what came with Rails.

I have recently started to upgrade Leavetrack to rely more on remote form submission and AJAX requests and wanted to put in place some system tests.

I started with the default Rails stack but quickly ran into some problems relating to running the tests in SSL using Puma. You can see some of the issues here, here and here.

None of these problems quite matched my own and despite binding Puma to run HTTPS, the tests would just hang.

I can't quite recall how I came across cypress.io but it offers end-to-end testing written in Javascript and with a few steps, it's easy to integrate into Rails.

1. Install and configure Cypress


The installation instructions from the Cypress website are really good. As I'm working solo, I installed via npm rather than adding it as a dependency in my Yarn package.

Installing Cypress will create a /cypress directory in your Rails app with a number of example spec files and other related folders.

The key file you need to review is the cypress.json file. In particular, you need to set the baseURL for your application.

You will see below that I set Puma to run on localhost:9292 so this goes into my Cypress configuration file.

2. Install the cypress-on-rails gem


This is super-helpful from the team over at Shakacode. It provides commands for Cypress that enable the use of Rubygems such as database_cleaner and FactoryBot. In effect, you can load fixtures, create factories and teardown the database as you would in Rails tests. Add to your Gemfile:

gem 'cypress-on-rails', '~> 1.0'

Then:

bundle install

This will create a number of additional files in your /cypress directory. These files allow the creation of factories or fixtures in Cypress spec files.

The README on the Github page for the Rubygem is excellent and following it through will get you setup.

3. Make sure you connect to the correct database


Despite the big old warning on the gem page, I did manage on one occasion to connect Cypress to my main development server and erase my development database. This at least gave me cause to write out a seeds.rb file.

There are two options to ensure connecting to the correct database. The first, which I started with, involved setting an environment variable that was used to determine the database to connect to. Updating database.yml to read:

database: <%= ENV['CYPRESS'] ? 'leavetrack_test' : 'leavetrack_development' %>

After the aforementioned development database issue (forgetting to set the environment variable), I decided to move to another approach. For historical reasons, I run my local version of Leavetrack using Apache and Passenger with a development SSL setup so it made sense, for Cypress, to use Puma as it's a lot simpler to get the server running in test mode.

Your mileage may vary on the below depending on your own configuration but starting Puma in test for me is executing the following command:

rails s puma -b 'ssl://127.0.0.1:9292?key=server.key&cert=server.crt' -e test -p 9292

4. Write your first spec file


For my first spec file, I went for something easy that didn't require much in the way of initialising the database - signing into the application.

describe('Logging in to Leavetrack', () => {

  beforeEach(() => {
    cy.app("clean")
    cy.appFixtures()
  })

  it('Logs in with valid credentials', () => {

    cy.visit('/login')

    cy.get('#user_session_username')
      .type('michael.scott@example.com')

    cy.get('#user_session_password')
      .type('password')

    cy.get('#new_user_session input[type="submit"]').click()

    cy.url().should('include','/dashboard')
  })

})

The spec above is pretty self-explanatory so let's move on to something a bit more complex.

5. Writing a more complex Cypress spec


For this, we'll look at doing one of the fundamentals in Leavetrack - booking some time off. This is a protected action so our spec will need to authenticate and then handle a form shown in a modal which is submitted via AJAX to the server.

const username = "michael.scott@example.com"
const password = "password"

Cypress.Commands.add('loginByCSRF', (authenticityToken) => {
  cy.request({
    method: 'POST',
    url: '/user_session',
    failOnStatusCode: false,
    form: true,
    body: {
      user_session: {
        username: username,
        password: password
      },
      authenticity_token: authenticityToken
    }
  })
})

This initial code sets the variables we will use to login and then defines a strategy for logging in that works with Rails. The guides on the Cypress website have a number of these strategies but we are  going to login with username/password, submitting the Rails authenticity token at the same time.

To do that, we run this command in a before filter.

beforeEach(() => {
  cy.app('clean');
  cy.appFixtures();

  cy.request('/login')
  .its('body')
  .then((body) => {
    const $html = Cypress.$(body)
    const csrf = $html.find('input[name="authenticity_token"]').val()

    cy.loginByCSRF(csrf)
    .then((resp) => {
      expect(resp.status).to.eq(200)
    })
    cy.getCookie('_leavetrack_session').should('exist')
  })
})

In the before filter, we clean the database and then load our fixtures. This command - cy.appFixtures() - is added by the cypress-on-rails gem. We grab the authenticity token from the HTML and then use our command to login. We confirm this has worked by asserting that we have a valid session cookie.

We'll now move on to actually testing the absence creation flow.

context('Creating a one day absence', () => {

  it('creates the absence', () => {

    cy.visit('/dashboard');

    cy.contains('New Absence Request').click();

    cy.get('#absence_from').click();

    cy.get('p.is-available').first().as('selected');

    cy.get('@selected').click();
    cy.get('@selected').click();

    cy.get('@selected')
      .invoke('attr','id')
      .then(id => {
        cy.get('#absence_from').should('have.value', id)
        cy.get('#absence_to').should('have.value', id)
      })

    cy.get('#absence_start_meridiem').should('have.value', "AM")
    cy.get('#absence_end_meridiem').should('have.value', "PM")

    cy.get('#new_absence #picker svg').click()

    cy.get('#absence_employee_notes')
      .type('Looking to take a break.')

    cy.get('#new_absence button[type="submit"]').click()

    cy.get('#new_absence button[type="submit"]').should('include.text','Success!')
  })

})

You'll see the syntax is quite expressive and it's not difficult to follow what the spec is doing. A lot of this is of course specific to my application but a couple of things to note in particular:

  • the form uses a date picker and we can test that clicking the first date in the date picker - at the time of writing, 1 May 2021 - correctly populates the relevant form fields;
  • on a successful (status: 200) request to the server, we callback to update the text in the submit button so we can then test that the text of the submit button has changed. On an unsuccessful request, the server returns status: 422 so we can be confident that the request has succeeded.

6. Running the tests


I've always loved running tests in a browser so my preferred approach is to have the local Cypress app open especially when writing tests.

It is of course possible to run them in headless mode and if you sign up to an account on Cypress, you get a great dashboard showing the status of the tests and recordings.

Cypress dashboard


You can see why this approach to testing is really important in this six-minute video.

Next steps are to increase the test suite coverage for existing functionality. In particular, I need to test failing scenarios more thoroughly.

For now though, I will leave you with a vide of Cypress running the above "create_absence_spec" locally. This video was automatically saved by Cypress and loaded to their dashboard for review if needed.



If you have any questions, you can find me on Twitter at @robinjfisher and @Leavetrack.

Posted by Robin on 14 May, 2021 in Leavetrack Engineering