Set up Phoenix acceptance tests with Cypress

Created: August 03, 2019 20:32 | Updated: September 05, 2019 17:01
Tags: Elixir, Phoenix

I really didn't expect this to work as well as it did in the end, patched together with a few shell scripts! Cypress is a very slick end to end testing framework and Phoenix needs a good way to do acceptance tests. The two Elixir alternatives are Hound and Wallaby (both of which I struggled with before landing on Cypress).

What this solution gets at the end of the day is a single threaded series of Cypress tests that can be run with a single command. I hope to improve this and get multiple threads running later, but for now it is at least convenient for Continuous Integration and the occasional developer check.

Let's get started!

Install Cypress

The binary can be pulled in using npm (don't forget to return to your main app directory afterwards).

cd assets/
npm install cypress --save-dev

Add systemtest configuration

Add a new config file: /config/systemtest.exs. This will give us a 4th environment, and a 4th database which will persist between system tests and be wipeable. It is very similar to the test.exs except with a different database name, server: true and no sandboxing.

# /config/systemtest.exs

use Mix.Config

# Configure your database
config :yourapp, YourApp.Repo,
  username: "postgres",
  password: "postgres",
  database: "yourapp_systemtest",
  hostname: "localhost",
  show_sensitive_data_on_connection_error: true,
  pool_size: 10

# We don't run a server during test, but we need it for systemtest!
config :yourapp, YourAppWebWeb.Endpoint,
  http: [port: 5000],
  server: true

# Print only warnings and errors during test
config :logger, level: :warn

Add shell scripts

There are three shell scripts that are very similar... one runs Cypress in the console, and the other opens up the Cypress window so the developer can visually run through the test. Put these in the main app directory.

# cypress-run.sh

MIX_ENV=systemtest mix ecto.reset
echo "===STARTING PHX SERVER==="
echo "===IF STARTING CYPRESS FAILS==="
echo "===RUN npm install cypress --save-dev ==="
echo "===IN THE assets/ FOLDER==="
MIX_ENV=systemtest mix phx.server &
pid=$! # Store server pid
./assets/node_modules/.bin/cypress run
result=$?
kill -9 $pid # kill server
echo "===KILLING PHX SERVER==="
exit $result
# cypress-open.sh

MIX_ENV=systemtest mix ecto.reset # Need a blank database
echo "===STARTING PHX SERVER==="
echo "===IF STARTING CYPRESS FAILS==="
echo "===RUN npm install cypress --save-dev ==="
echo "===IN THE assets/ FOLDER==="
MIX_ENV=systemtest mix phx.server &
pid=$! # Store server pid
./assets/node_modules/.bin/cypress open
result=$?
kill -9 $pid # Kill server
echo "===KILLING PHX SERVER==="
exit $result # Return test result
# cypress-ci.sh

MIX_ENV=systemtest mix ecto.reset
echo "===STARTING PHX SERVER==="
echo "===IF STARTING CYPRESS FAILS==="
echo "===RUN npm install cypress --save-dev ==="
echo "===IN THE assets/ FOLDER==="
MIX_ENV=systemtest mix phx.server &
pid=$! # Store server pid
./assets/node_modules/.bin/cypress run  --record --key 114a5558-7d13-4b81-a67e-3deb9e3f073d
result=$?
kill -9 $pid # kill server
echo "===KILLING PHX SERVER==="
exit $result

And set the bash scripts as executable (or else they are not going to do much)

chmod +x cypress-run.sh
chmod +x cypress-open.sh
chmod +x cypress-ci.sh

Add Mix aliases

And add the appropriate mix aliases to mix.exs

defp aliases do
[
    ...
      "cypress.open": ["cmd ./cypress-open.sh"],
      "cypress.ci": ["cmd ./cypress-ci.sh"],
      "cypress.run": ["cmd ./cypress-run.sh"]
]

Initialize Cypress

To create the folder structure you need to 'open' the Cypress window, Run the command now with the mix command.

mix cypress.open

Write some cool tests

Here is an example of what I am working on right now in cypress/integration/acceptance_test folder

describe('Acceptance Test', function () {
  beforeEach(function () {
    // before each test, we can automatically preserve the
    // 'session_id' and 'remember_token' cookies. this means they
    // will not be cleared before the NEXT test starts.
    //
    // the name of your cookies will likely be different
    // this is just a simple example
    Cypress.Cookies.debug(true)
    Cypress.Cookies.preserveOnce('_health_key')
  })

  it('Visits the main page', function () {
    cy.visit('http://localhost:5000')
  })

  it('Registers an account', function () {
    cy.contains('Register').click()
    cy.get('input#user_email').type('test@test.com')
    cy.get('input#user_name').type('Testy McTestface')
    cy.get('input#user_password').type('securepassword')
    cy.get('input#user_confirm_password').type('securepassword')
    cy.get('[type=submit]').click()
    cy.contains('Welcome back Testy McTestface')
  })

  it('Enters a weight record', function () {
    cy.contains('Weight').click()
    cy.get('#trend').find('input#log_weight').type('234')
    cy.get('#trend').find('[type=submit]').click()
    cy.contains('Recent Logs').click()
    cy.contains('234.0')
  })
})

Note that I am directing the test to go to localhost:5000 (as specified in the systemtest.exs), and I am registering (because the database is empty). Maybe I will get more sophisticated in the future, but for now this works just fine!

CI

I added the following commands AFTER my regular tests in CircleCI

      - run: cd assets && npm install && npm install cypress --save-dev && cd ..
      - run: mix cypress.run

Note that npm install is not enough by itself, you need to run npm install cypress --save-dev properly install.

Running the test concurrently with everything else

I am completely enamoured with ex_check for running my checks concurrently. You can add cypress to your .check.exs file with

{:cypress, command: "mix cypress.run"},

This will add some time to your CI, so I recommend using dialyzer as well to make the time worth it!

End

This should work on CI and in your terminal with mix cypress.run, hope this helps someone!

Written by Alan Vardy. Let me know how I can make this better!