A day with Cypress

Preface

Thanks everyone for the feedback I’ve had since the publication of this post, it’s been very interesting reading the various opinions and questions.

Following up on that feedback I wanted to make a couple of things clear about the post:

  1. This is not a ‘best practices’ guide. I think there are some good practices below, but as with all good practices, they need the right context. If you’re new to writing code, it’s probably better to get a good grounding in the basics before applying much of what I cover, otherwise you may end up heading down the wrong direction.
  2. The introduction of Cucumber to Cypress is to satisfy my own technical curiosity and should not be a first step when adopting/looking in to Cypress. I’ve long lost my enthusiasm for the gherkin syntax and avoid it where I can, but it can have its place and can be useful, but again that’s in the appropriate context.

My advice would be to take the post on face value. It is description of what I did, rather than a recipe for introducing Cypress to your testing tool set.

If you’ve got any questions on the above and how it applies to the post below, feel free to get in touch either here or @StooCrock on twitter.

The Fates Align

Sometimes you just have to take advantage of the trials the universe throws at you. Yesterday, just before heading out the door and getting on my bike, I did something I would normally not do: I paused and decided to check whether my train was running. There are strikes on at the moment and whilst I was expecting my usual 7am train to be off the schedule, I thought I’d better check that the schedule hadn’t changed further.

Am I glad I did! There ended up being multiple issues on the line, meaning it was highly unlikely I was going to get on a train for a few hours. I was already dressed and raring to go so I thought to myself, let’s spend some time with Cypress.io, because that’s what you do at 6:45am right?! That few hours turned in to a full blown WFH day and this is what I did with it.

I’d used Cypress a little in the past, and some of the teams in my platform use it, so I knew what I was going to get up-to. This was going to be a refresher course but with stretch goals:

  1. To put a Cucumber implementation on top to author the scripts
  2. To look again at stubbing calls at runtime
  3. To look at using snapshots instead of explicit assertions

With a test application at hand, I started putting together a simple set of acceptance checks after installing the cucumber plugin and deciding how to organise my scripts. As with most Cucumber implementations, it’s the quirks which slow you down, especially when the library used is still maturing. In this case I went for The Brain Family Cucumber plugin, mostly because it’s linked to from the Cypress site 🙂

I’m not going to repeat the setup instructions, but instead will say it was pretty easy to get things to a point where I could write a feature file and my first step definition.

I didn’t think too much about the organisation of the project at this point, but I’m sure it’ll be something I consider going forward, so I stuck with the default suggestions.

My general approach to writing any code is to make it work first, then make it better later, in this case, using the checks themselves to help me make incremental refactors. I more often that not have a good idea of ‘what good looks like’, but I’m happy to get there organically, changing my mind where necessary, as I learn and adapt.

Cypress & Page Objects

With Cypress, the first opportunity for refactoring rears its head when it comes to abstracting framework implementation details away from the checks themselves. Cypress doesn’t have its own implementation of Page Objects, unlike other frameworks such as Nightwatch.js.

The Cypress documentation is clear for me, but I can understand that it might be a little daunting if you’re early on your code writing and/or automation journey.

Cypress say:

> Can I use the Page Object pattern?

Yes.

The page object pattern isn’t actually anything “special”. If you’re coming from Selenium you may be accustomed to creating instances of classes, but this is completely unnecessary and irrelevant.

The “Page Object Pattern” should really be renamed to: “Using functions and creating custom commands”.

I interpret this as: there’s no point in us providing something which javascript inherently does, so do what you think is right.

Great, so where does that leave us? First up there are selectors, lots of little bitty pieces of text that we pass in to a function to help us find something on the page. We tend to abstract these out to Page Objects, but if taken on face value (they encapsulate a whole page), have a tendency to become unwieldy very quickly. For some pages that’s a whole lot of selectors. I prefer to abstract what’s unique about the page in to its own object (normally it’s base level constituent parts), and then where it makes sense, have logical components of that page separated in to their own individual objects.

An example…

Let’s create an example using the Cypress.io home page.

If you inspect the home page you’ll find that it’s made up of discreet sections stitched together. There’s a navigation bar, a hero image, a host of ‘sections’ and a footer. Let’s assume that in this instance, the home page has to have each of these sections in order for it to be a well formed page. So, if I’m going to check that the home page is constructed correctly, I could in this instance, decide that as long as those components exist, then what’s in them is initially irrelevant. My check therefore only goes one or two levels deep, at most.

Let’s start by writing the Cucumber Scenario for this check and implementing the step definitions for it without any abstractions.

Firstly this is our scenario, which we’ve put in a file called cypressHome.feature

Feature: The cypress home page

Users can visit the cypress home page

Scenario: Opening the cypress home page in a browser
Given I open the Cypress home page
Then it has a navigation bar
And it has a hero image
And it has an end to end section
And it has a footer

And now the associated step definitions which we’ve put in a file called cypressHomeSteps.js

const url = 'https://cypress.io'

Given('I open the Cypress home page', () => {
cy.visit(url)
})

Then('it has a navigation bar', () => {
cy.get('.navbar')
})

Then('it has a hero image', () => {
cy.get('#hero')
})

Then('it has an end to end section', () => {
cy.get('#end-to-end')
})

Then('it has a footer', () => {
cy.get('#footer')
})

These selectors we’re using seem like good candidates to abstract away in to a component, let’s call it cypressHomePage.js and put it in a components folder in the project tree.

In this file we’re going to export an object that we can then reference in the step definition file. Here’s what it now looks like.

module.exports = {
NAVBAR: ".navbar",
HERO: "#hero",
END2END: "#end-to-end",
FOOTER: "#footer"
}

To reference this object, we can import it in to our step definitions file. The path to the file is important, so make sure that you adjust it for your folder structure.

Once we’ve imported the object and updated the step definitions, our modified file now looks like this:

const url = 'https://cypress.io'
const { NAVBAR, HERO, END2END, FOOTER } = require("../../components/cypressHomePage");

Given('I open the Cypress home page', () => {
cy.visit(url)
})

Then('it has a navigation bar', () => {
cy.get(NAVBAR)
})

Then('it has a hero image', () => {
cy.get(HERO)
})

Then('it has an end to end section', () => {
cy.get(END2END)
})

Then('it has a footer', () => {
cy.get(FOOTER)
})

Now we have a defined set of selectors in a component which can be reused across the rest of our step definitions if needed. The benefit is should those selectors change, we only need to change them in one place. A secondary benefit is the references are semantically valid, making them easier to read and in the case of some crazy length selector text, they take less space 🙂

Note…
In this example, I’ve included the URL as a const. This is purely to help highlight some changes later in the post. A better practice and one built in to Cypress is to have the URL in the cypress.json file as a value for the property ‘baseUrl’. When navigating to the page, you would then use cy.visit(‘/’), as the URL would be automatically included. This becomes more powerful as you require the use of more pages in your application as they’d be referenced as a friendlier and more readable cy.visit(‘/myaccount’) etc.

Taking another step

All very basic so far, but that’s the best thing about it. Having singular steps for each section is nice to a point. It’s explicit so we know exactly what is going on without the need to dig deeper in to the code base. But it’s a little wasteful, so let’s take refactor again. This step might be a little controversial but I like it.

Remembering our scenario from earlier, I’m suggesting we make a little change:

Feature: The cypress home page

Users can visit the cypress home page

Scenario: Opening the cypress home page in a browser
Given I open the Cypress home page
Then it has the necessary sections

I can hear the audible gasps! Some might say that I’ve broken a fundamental rule of the Gherkin syntax, by removing the explicit expectation from the scenario and condensing it in to something a little more fluffy. Whether you want to do this, or not is up to you, but go with me for this example.

If we head back to our step definitions, we can now reduce that down to these:

const url = 'https://cypress.io'
const { NAVBAR, HERO, END2END, FOOTER } = require("../../components/cypressHomePage");

Given('I open the Cypress home page', () => {
cy.visit(url);
})

Then('it has the necessary sections', () => {
cy.get(NAVBAR);
cy.get(HERO);
cy.get(END2END);
cy.get(FOOTER);
})

The changes are in but I’ve not really done anything different… yet. When building a framework, we want to try and keep our checks, and the library we’ve chosen as separate as possible. Up until now our scenario step definitions have been tightly coupled to Cypress, but we can do something about that by extending our component.

What we’ll do is create a function in our component that we’ll call from the scenario step definition. We’ll call this function ‘hasTheNecessarySections’. Because the function lives within the same object as the properties we created earlier for each of the sections, we’ll need to reference each with this.. We’ll also do what we suggested earlier, and do the same thing for navigating to the page in the first place, by adding another function named navigateTo.

module.exports = {
NAVBAR: ".navbar",
HERO: "#hero",
END2END: "#end-to-end",
FOOTER: "#footer",
URL: 'https://cypress.io',
navigateTo: function() {
cy.visit(this.URL)
},
hasTheNecessarySections: function() {
cy.get(this.NAVBAR);
cy.get(this.HERO);
cy.get(this.END2END);
cy.get(this.FOOTER);
},
}

And in our step definitions we’ll change how we import the component and call the new functions in the appropriate steps.

const cypressHomePage = require("../../components/cypressHomePage");

Given('I open the Cypress home page', () => {
cypressHomePage.navigateTo();
})

Then('it has the necessary sections', () => {
cypressHomePage.hasTheNecessarySections();
})

And that’s it. It’s a simple but effective way of abstracting away implementation details from our checks, using component based objects even though Cypress doesn’t support them natively.

Taking it even further

At the beginning of the post I mentioned the 3 areas I wanted to look in to. I’ve covered most of goal 1, demonstrating how to use Cypress with a Cucumber plugin and a bonus section on abstracting implementation details from your scenarios. One area I’ve not covered is the use of tables in scenario steps. For those familiar, tables allow you to enter test data that a step should iterate over, with related outputs if necessary. I’ll cover the use of them in a later post.

What I haven’t done is touch on goals 2 or 3. I was planning to, but this post got a bit on the long side. Next time I’ll demonstrate how to use the cy.server() and cy.route() APIs to stub data from APIs, allowing us to make best use of the snapshot capabilities provided by the Cypress Snapshot library. Using this library we’ll remove the need to explicitly call cy.get() for each section of the home page and replace them with a single call to a saved snapshot. In doing so we’ll be able to cover the whole of the page (if we want to), not just the elements in the markup, but the appropriate data as well.

I hope this has been useful for you. Leave a comment if it has or if there’s anything you disagree with.

Thanks for reading.

2 thoughts on “A day with Cypress

  1. There are a couple of things I would hope to see people with influence in the community change, and only then we will hopefully shape new professionals when it comes to setting up test suites (yes I get a bit disappointed in inheriting overcomplicated test suites because posts like these could mislead less experienced professionals):

    1:. In a introduction post to a new tool, I would probably not use cucumber as an example. Cucumber serves a purpose to support BDD practice, which is meant to solve a very specific problem and allows engineers to write automated tests from ACs written in gherkin. So in these cases an example using cucumber is great! The problem here is that many places do not apply BDD practices in their day to day delivery, although because a less experienced professional was introduced to the tool using cucumber, they will blindly use it unnecessarily overcomplicating the test suite. So maybe the good and old baked in Chai & Mocha example would be nice, or give a small introduction when to use cucumber, maybe a reference to a post?

    2:. I agree abstracting the selectors, anything else though will get you into the pageObject pattern trap which is a big NO NO, won’t add much reasoning of why to this because I am sure you are aware, but maybe who is reading this will think it’s ok. For example, instead of adding functions to `cypressHomePage` (maybe to reinforce this module only takes selectors could be called homePageSelecotrs? or live under a folder called selectors?), create a custom command named `hasTheNecessarySections` and in your test you call it `cy.hasTheNecessarySections`, that’s a better and preferred practice.

    3:. I know you purposely avoided setup steps, but I found most people new to a tool and with less experience struggle on the setup rather then creating the test itself. Also a way to guide your readers of how you can get proficient on a new tool would be giving an example of the implementation and then referencing it to cypress documentation, that would then lead people to understand better where to find the source of the information so they could carry on ramping up on the tool.

    The above is my opinion, it could be totally wrong in your point of view or not, either way I thought it was worth sharing.

    Good post though, thanks for sharing!

    Like

    1. Hey Filippo, thanks for taking the time out to share your thoughts. I’m unsure however what to do with it, so I hope you can help me. I’ve got some questions, comments below for you to consider following up on.

      1. Your opening paragraph, is it aimed specifically at my post or posts in general?
      2. I will make an amendment to the post to suggest further that I’m not overly fond of cucumber myself, but I was interested in understanding the technical challenge of what it would take to get it up and running. Despite our agreement on cucumber & bdd, the reality is lots of places do use it. If this post helps someone to implement cucumber on top of a browser automation library when they’re under pressure to do so, then I think its served its purpose. I cannot overly influence how someone/a team does or does not use the gherkin syntax nor was I trying to.
      3. In my experience and with the engineers who’m I’ve worked with, I’ve found most believe that unnecessary abstraction and de-coupling is a burden with little payback. In this case, the hasNecessarySections method may only exist because of the component that we’re asserting against. Having that decoupled from the component and called without context, reduces the semantic qualities of the code. Also, should that component no longer exist in the application, in your example there is a risk that it still hangs around despite the scripts & component objects having been removed. Therefore I disagree that it’s a better and preferred practice. It might be for automation engineers, but then in my experience very few of them have been good software engineers. There are no best practices, only good practices used in the right context. In your context your solution might be the right one, in the context of this post, it’s not. I did say in the post that I prefer to make things work, then make them better. That is not a single loop process and it may be that when the context changes, your proposal would be the right thing to do. Again, in this context, that is not the case. But I appreciate your thoughts.
      4. Given that you knew that I avoided setup steps, and at the bottom of the post said that there was at least 1 additional follow up, I’ll take your feedback and will apply it to that post, when I get around to writing it 🙂

      Like

Leave a comment