DEV Community

loading...

Cypress testing with Rails API Backend

tomsfernandez profile image Tomas Fernandez ・4 min read

At one of our current projects we started testing our key functionalities with Cypress for our React front-end. As we started writing our tests we stumbled upon a problem, how could we have a clear and easy way to setup and clear the data created/modified in the test in a secure way?

We couldn´t do it directly in our API as our front end is fed by several APIs. We also wanted our tests not to be written in Ruby so the cypress-on-rails gem wasn't an option.

Our main goal was to do this without having to add unwanted functionality to our Rails API.

Our first approach was to create a Rails controller that would only be enabled for Cypress testing. We chose to secure it via environment variables and an authentication token.

This option had one big pro: it was very easy to create and easy to maintain, it was only a very big controller in the middle of our production code.

Sadly, there were a number of drawbacks:

  • Code meant as test helper was in the middle of the production and business code.
  • We had to be really careful about the controller's security. Having a controller able to delete cascade several of our entities was a huge risk to have.

The last point was really important to us. A mistake in the APIs deployment could lead to someone wiping half of our database!.

That's why we looked into another option: using Rails engines.

The pros:

  • Active Record models from our main app can be referenced from the engine.
  • Endpoints used by Cypress won't be available in production.
  • The code of the endpoints won't even be available in the production bundle.

This really lowered the chances that a mistake in deployment configuration left the endpoints available.

The code

Lets create a new Rails project first:

$ rails new cypress_api --api

Inside our newly created API, lets create our Engine to declare our cypress-specific endpoints:

$ rails plugin new cypress --mountable

This will create a cypress folder at the root level with our new Rails engine and will also append the gem configuration in our main Gemfile.

Run bundle install and remove the TODOs from the engines gemspec to be able to start the app.

Now, lets create a new controller inside the main app to signal the API is active. Write this code into app/controllers/status_controller.rb:

class StatusController < ActionController::API

  def status
    render json: {"status": "OK"}
  end
end

Let's do the same inside the engine, but with a different message. Inside cypress/app/controllers/cypress/status_controller.rb:

module Cypress
  class StatusController < ActionController::API

    def status
      render json: {"status": "Cypress OK"}
    end
  end
end

Also, add the corresponding route config in config/routes.rb and cypress/config/routes.rb as well.

Last, we have to mount our engine's endpoints to our main app. The thing is, we can't do that statically as we don't want the endpoints to be available at all times. We have to do it dynamically.

To do this, we have to add this snippet to the Engines main class in cypress/lib/cypress/engine.rb:

  config.cypress_engine = ActiveSupport::OrderedOptions.new
  initializer 'cypress_engine.configuration' do |app|
    if app.config.cypress_engine[:mounted_path]
      app.routes.append do
        mount Cypress::Engine => app.config.cypress_engine[:mounted_path]
      end
    end
  end

So what does this snippet do? If the config.cypress_engine option is declared anywhere in our environment files or application file the engines endpoints will be appended to the main app with a prefix.

For now, let's declare that option in our development.rb file.

 config.cypress_engine.mounted_path = "/cypress"

We are now declaring that our Cypress Engine endpoints will be mounted into our main app with the cypress prefix.

Let's try it out! Start Rails in development mode and access "/status" and "cypress/status". We can see that both endpoints are available!.

Now, lets start the app in production mode. If we try to access "/cypress/status" we can see that 404 is returned.

Great! Now the Cypress endpoints are not available in the production environment but is still bundled into our app. To omit adding the endpoints to our app while in production we can limit the Gem to only be installed while in development.

  group :cypress do
    gem "cypress", path: "cypress"
  end

We can also add another level of security to our engine in case someone removes this config by mistake.

Add this snippet to cypress/lib/cypress/engine.rb below what we previously added:

  initializer "cypress_engine.cypress_only" do
    unless Rails.env.development?
      abort <<-END.strip_heredoc
        Cypress Engine is activated in the #{Rails.env} environment. This is
        usually a mistake. To ensure it's only activated in cypress
        mode, move it to the development group of your Gemfile:

            gem 'cypress', group: :development
      END
    end
  end

If the cypress engine is used in an environment other than development, Rails startup will be aborted.

You can find all the code in this repo:

GitHub logo tomsfernandez / Rails-Cypress-API-Example

Example code of a Cypress setup for Rails API with Engines for Dev.to blog post

(In the repo a new environment called "cypress" exists to add another level of security but it isn't really necessary)

Discussion

pic
Editor guide
Collapse
domi91c profile image
Dominic Nunes

Very helpful for my use-case, thanks for this.