Jan 18, 2019
Cypress is the up and coming framework for end to end testing. It has grown a lot in the last year or so and seems to be on track to become a feature-rich testing framework with an intuitive API that will replace Selenium based frameworks in many places. It has plenty of industry support and is used by many large organizations. It offers the possiblity to rapidly write e2e tests which interact with your web app. It is easy to extend, to write custom commands and to mock requests and responses.
The only browser supported at the moment is Chrome (or the chromium based electron), which isn’t necessarily a problem for us, as we recommend our users to use Chrome anyway.
In this post I will tell you a little bit about our codebase and the challenges we encountered implementing Cypress into it. I will also reflect on how it allows us to shorten the feedback loop between developing and deploying. We still have some way to go, but are already seeing the benefits.
Example of a test running in Cypress
Our app grew organically over the years and has become quite complex with lots of moving parts. Especially the graphical editor, where our clients can design detailed solar panel systems on top of an aerial image of a building or field, has lots of features that are highly integrated with each other. Before 2018, we used to test these features manually with every biweekly release, to check if everything was still functioning. We had 8 release test scenarios which an employee had to go through step by step to make sure everything was still working properly, along with someone spending some time as a general bughunter, who tried breaking the app. This had several advantages:
But there were also many disadvantages:
We decided to look for ways to start automating our release testing. At first, we spiked a branch where we used Selenium. This quickly became very tedious and inspired us to start looking at other solutions. Ultimately, we settled on Cypress, which was easy to set up and get running.
We started with implementing a test for a simple scenario. This helped us get acquainted with the way Cypress works, learning about the proper way to select elements on the page, build commands that handle repetitive tasks, wait for responses from our backend api, etc.
We’ve come across several challenges while implementing Cypress, first a couple that are specific to our app:
Then some that are more general:
We’ll go over these one by one.
We have a Python/Django backend and use sessions rather than tokens. Locally, we started with just adding an admin cypress user to our development database. But when we deploy a release candidate to our beta server for testing, we don’t want to have such a user there with admin privileges. So for now we still have to settle for getting a session id manually and passing this in as an environment variable to test things on beta.
We wrote a custom command, which visits the login page, and logs in when working on localhost, but when testing against a web server we require a session id to be passed in:
cypress open --config baseUrl={url-of-server} --env sessionid={session-id}
We would like to switch to using tokens, and perhaps to expose an endpoint on beta which can generate temporary access tokens for use during tests, but haven’t implemented this yet.
This was the most complex problem we faced. Accessing objects' locations that get drawn on an html canvas, some of which are only visible when a user’s mouse is in a specific location posed some challenges.
Finding the objects was solved by exposing the fabric canvas on which they were drawn on the global window object when we are not in production. This allowed us to find grids, panels, and the buttons that are used to manipulate those in our editor. We wrote several helper functions to make the tests themselves look more natural, but it was still quite complex and in need of commenting:
cy.window().then((window) => {
const fabricCanvas = window.fabricCanvas
// Move one grid so it definitely won't cover the other
// getGrids and getPosition are helper functions that
// grab the grids on the canvas and gets the position of
// an element respectively.
const grid = getGrids(fabricCanvas)[0]
const { x, y } = getPosition(grid)
cy.get('#editor-map')
.trigger('mousedown', x, y)
.trigger('mousemove', x + 100, y + 100)
.trigger('mouseup')
})
You see that it is still quite nuts-and-bolts when it comes to clicking on the editor (in this case we click on a grid, move it 100px right and down and release the mouse). A next step we did, therefore, was to generalize this idea and move some of this code to a command:
// Add a command where we grab the fabricCanvas
Cypress.Commands.add('withFabricCanvas', () => {
cy.window().should('have.property', 'fabricCanvas')
cy.window().then(window => window.fabricCanvas)
})
// Add a command to move a grid, which can be chained onto the previous one
Cypress.Commands.add('moveGrid', { prevSubject: true }, (grid, offset) => {
const { x, y } = getPosition(grid)
cy.get('#editor-map')
.trigger('mousedown', x, y)
.trigger('mousemove', x + offset.x, y + offset.y)
.trigger('mouseup')
})
// Now the code in the test becomes
cy.withFabricCanvas()
.getGrid(1) // gets grid with id 1
.moveGrid({ x: 100, y: 100 })
One of the biggest gotchas when writing Cypress tests is that eventhough the code runs asynchronously, the structure of the code is synchronous, as if we are using async/await without the keywords:
cy.visit('/hello/world') // blocks until the document is loaded at that url
cy.get('#button').click() // gets the button and clicks it
cy.get('.message').should('contain', 'Hello World!') // do assertion
Normally one would expect asynchronous actions to complete after the next line of javascript, but with cypress, all these actions are blocking, with a timeout determined in the configuration. This allows us to write code in a structured way without having to create Christmas tree after Christmas tree or .then()
after .then()
. But it did require some getting used to. One has to reason differently about the code.
We took some initial steps to get Cypress to work in our CI. The first step consisted in making the tests run inside a docker container instead of on our local machine. It should be possible to replicate this on the CI and run the tests there with the appropriate alterations to our configuration files.
At the moment we have our tests working properly in a local docker container. For this we built upon the example on GitHub by Gleb Bahmutov, one of the lead developers at Cypress. Integrating it into our CI seems not too difficult, although we didn’t push forward on this just yet. Some attention needs to be put into minimizing the build times on Codeship, so a direct feedback loop for us developers remains available.
This has probably been the biggest challenge, and the most abstract. We learned how to think about testability in a different way. Before, the end to end tests were more separated from developing time, but now we are reaching the point where we can start getting more frequent feedback on how a code change influences the way our app works. This will improve the developer experience as well as increase the knowledge the dev team holds. We can start working in a more directed way because of this.
We can now write tests for new features immediately and make sure they behave as intended. We haven’t applied a complete TDD approach to developing new features just yet, as often the way things behave in our editor aren’t completely specified when development starts. In the best case these tests can be written at the same time and in the worst case afterwards. This gives us the confidence that our app will keep working for our users. We need to instill these new habits as developers.
Another thing is that we are taking a full stock of our features and aim for a high testing coverage for these. Things that have slipped through the cracks before can now be tested.
So there you have it. In our opinion, Cypress saves us a lot of work. It helps us to catch regressions and maintain functionality across our app. It also forces us to think ahead of time about how we will implement certain new features, which will result in smarter decisions over time.
The biggest hurdles have been overcome. In the future, we would like to do the following (in no particular order):
We’re very enthusiastic about Cypress and the possibilities it offers. Our software at Solar Monkey will be better because of it.