Automated End-to-End Testing with Cypress and Husky Pre-Commit hooks

Ankit Trehan
10 min readMar 8, 2021

How you can use Cypress and Husky together to build a pre-commit testing hook for your application

What comes to your mind when you have to write test specifications for your front-end application? Annoyance? Boredom? Well, be annoyed no more because Cypress is here to make your testing lives fun and exciting. So without further ado, let’s get into why Cypress is such a great tool and how we can use it to create our pre-commit hook.

What is Cypress?

Cypress is a modern Javascript-based testing framework that can be used to automate end-to-end testing. The project’s website puts it perfectly, Cypress is for “fast, easy and reliable testing for anything that runs in a browser.” It has a ton of benefits over traditional testing automation frameworks and also comes with numerous features right out of the box, which we will go over in-depth in this article.

Why should I use it?

1. Easy Setup

Out of the tools that I have used to test applications, Cypress is by far the easiest, and quickest to setup. All you need to get started is one npm or yarn command (we will go over how to set it up in the second half of this article). Compare this to something like Selenium which requires an extensive set up and has a bunch of dependencies, making the process more complex than it needs to be.

2. Written in Javascript

If you are a web developer, you are most likely comfortable with writing code in Javascript and, or your product itself is in Javascript. Cypress tests are written in Javascript which means that you might not have to pick up a new language to write your tests like you would have to for other conventional forms of front-end testing.

Bonus: If you know how to write tests in other Javascript frameworks like Mocha, you will have a smooth experience picking up Cypress since it is based on top of those tools.

3. Comes with a GUI

Cypress has a debugger and a GUI to show how your tests are doing and the ones that have failed/passed. It gives you a real-time look into what is happening with your tests and allows you to pause, resume and rerun the tests straight from the interface. It also comes inbuilt with an element selector. This can help build your tests quicker since it eliminates the time spent selecting elements, like in other frameworks.

4. Hot reloading

Cypress tests automatically reload when you save changes to your spec file. Although this might not seem like a big deal, it helps with not having to run commands over and over again to run the same tests, saving a lot of time.

5. Waits for components to load

Tired of specifying every line of your tests to be asynchronous? Cypress has you covered. It automatically waits for all the components to load, waits for commands and other checks before moving on. You can change this wait time that it automatically applies in the package.json if you are feeling the need to increase it. Here’s to never writing waits again!

6. Ability to go back in time

Cypress will give you superpowers, not just testing superpowers but the ability to go back and see how the test ran. The GUI allows you to go back and look at what the state of your page was when tested and where the test went from there. This is one of the best features of it hands down because you can point out exactly what improvements need to be made / what other use cases need to be accounted for

7. Amazing Documentation

If you haven’t been convinced yet, Cypress’ documentation will make you want to use it. As developers, we appreciate solid documentation. Cypress provides that to us. You can follow tutorials on their website that will set you up for working within it. They also have a bunch of code examples and best practices around using each function.

In short, Cypress is a testing framework that not only makes testing easy and fun but also comes out of the box with a lot of features that will make you fall in love with it.

What are the drawbacks?

Honestly, it is difficult to think of any drawbacks, but after giving it a lot of thought I could come up with two.

1. Problems with single-page apps

While writing tests for certain single-page apps, Cypress had some difficulties recognizing that the components and the route of the page had changed.

2. Automated wait time might not be enough

Sometimes, the automated waiting that Cypress offers might not be enough for the components to load. Maybe you have a backend request which takes a while to process. This may cause the Cypress wait time to fail your test and you might have to specify custom wait times for certain components (you can see I am nitpicking now).

How do I set it up and use it?

Let’s start with creating a very basic react app. We will have a couple of form fields and buttons so that we can get a feel for how Cypress works. Use the following command to create a boiler-plate react app:

npx create-react-app cypress-medium

cd cypress-medium

npm start

We will use react-bootstrap for our components, you can install it using:

npm install react-bootstrap --save

Inside the cypress-medium directory, you can follow the code from my repository to build your own components. Here is a little snippet from that file to show how yours should like:

Note: We are using the useState hook to keep and update the state of our application, you can read more about it here.

Sample React App

To run through this real quick, we are setting our states using our hooks, then declaring if our submit button should be enabled based on the inputs in the form. We have a very basic form that uses components from react-bootstrap, and we also have a modal that will be an additional feature we can test with our framework.

Now that we have our react app set up, we can do what we have been waiting for, installing Cypress! You can use one of these commands in your project’s directory to install:

npm install cypress — save-dev

or

yarn add cypress --dev

Then to initialize Cypress, run the following command:

node_modules/.bin/cypress open

To run your tests in a headless manner, you can use run instead of open. Remember this should all be in your project’s directory for these commands to work.

After running this command you will see a new folder named “cypress” in your project. Cypress will generate a bunch of example files for you within the integrations directory. We will go ahead and delete all of them because we will be building a custom one for our application.

Next up, we are going to update our package.json to override npm test to run Cypress. In your package.json file, under the scripts object, update test to say test: "./node_modules/.bin/cypress open.

Let’s create a new example.js file inside the integration directory. This is where we will write our tests. We will have a very basic test that only goes to our react app.

describe(“Our first test”, () => {  it(‘should go to our sample react app, () => {    cy.visit(“http://localhost:3000/");})}

describe is the convention used to group together smaller tests and it s are supposed to denote the smaller unit tests.

This gives us a nice little demo of a Cypress spec. Now, we can dial it up a bit. Let’s try to select a component and type something in it. We can use the selector tool in the UI to do this. Click the button on the top left, click on the email field, and you will see how Cypress helps you select it.

We can now use this to type something into our text box. Use the following command to implement it:

cy.get(‘#formBasicEmail’).type(‘test’);

Some common methods that will help you test for basic components of your application are:

.should() — check for various thing.click() — click elements.contains() — checks if the DOM contains a string.url() — checks for URL properties

It’s that simple! Now we can build on top of this, and create a more in depth test. Let’s check for form validation logic. In our test, we want to first go to the webpage, check if our submit button is disabled and try some basic form inputs. We can achieve this with the following:

describe(“Our first test”, () => {  it(“should go to our sample react app”, () => {    cy.visit(“http://localhost:3000/");    cy.contains(“Cypress testing with Husky pre-commit hooks”);  });  it(“should see that the submit button is disabled”, () => {    cy.get(‘.submitBtn’).should(‘be.disabled’);    cy.get(‘.modalBtn’).should(‘be.enabled’);  });  it(“should enter some incorrect email value — submit should be         disabled and modal should be enabled”, () => {  cy.get(‘#formBasicEmail’).type(‘test’);  cy.get(‘.submitBtn’).should(‘be.disabled’);  cy.get(‘.modalBtn’).should(‘be.enabled’); }); it(“should enter some password value — submit should be disabled and modal should be enabled”, () => {  cy.get(‘#formBasicPassword’).type(‘test@test.com’);  cy.get(‘.submitBtn’).should(‘be.disabled’);  cy.get(‘.modalBtn’).should(‘be.enabled’); });  it(“should check the box — submit should be enabled and modal should be disabled”, () => {  cy.get(‘#formBasicCheckbox’).click();  cy.get(‘.submitBtn’).should(‘be.enabled’);  cy.get(‘.modalBtn’).should(‘be.disabled’);  });});

Okay so we have a good long enough file, but the beauty about Cypress is that it is extremely easy to read. Anyone reading the above code can understand what exactly is happening.

There is one little problem though; we have quite a few lines that are repeated. The disabled and enabled checks are repeated a few of times because we want to see if our form logic is working as expected. We can refactor these lines using custom commands to reduce the size of our code and make it more readable and reusable.

So to create a custom command in Cypress go to the support/commands.js file. You will see a few commented examples of such commands, for our tests we will name our custom command checkForm

Cypress.Commands.add(‘checkForm’, () => {   cy.get(‘.submitBtn’).should(‘be.disabled’); cy.get(‘.modalBtn’).should(‘be.enabled’);
});

Now we can use cy.checkForm() to do both the checks instead of specifying these everywhere. We reduced our lines of code being used by 50% in our main file. This is a very small example, but imagine reusing these commands for your production app. Custom commands can help reduce your code size significantly and make it easier for another person to follow.

Using the above custom command, and testing out our modal, the final result looks like this:

You might get a cross origin error while executing the above code. We can fix this by adding "chromeWebSecurity": falsein our cypress.json.

Alright, so if you run Cypress with the open option, you should see something like this:

Final Result

Now that we have successfully created our testing framework for the app that we are planning on releasing, we should be able to run these tests before we commit our code. This is where Husky comes in.

To install Husky pre-commit hooks in your application, run the following commands:

npm install husky — save-devnpx husky install

To add our npm test, which we directed earlier to run Cypress, to the hook, all we need is the following line:

npx husky add .husky/pre-commit “npm test”

Now try committing something to your Github repo. You should see the Cypress tests run and check for your app to pass.

One final change would be that I would recommend updating our package.json from:

test: "./node_modules/.bin/cypress open to

test: "./node_modules/.bin/cypress run

when you try committing you should see something like this:

Passing Test
Final Output
Failing Test

And there you have it. You have created an automated end-to-end test which runs on each commit to your repository!

Conclusion

Cypress is a tool that I highly recommend using because of its ease of use and the amount of amazing features that it packs. It also makes testing, an important part of development, fun and exciting. Overall, in my opinion, it is the best testing framework out there for anything on the web.

Coupling Cypress with a pre-commit hook, like Husky can make your life a lot easier because you can be confident that your application works the way it was intended to.

Happy Testing!

--

--