How End-to-End Testing finally makes fun

How End-to-End Testing finally makes fun

Cypress changed my viewpoint on E2E-Tests

In this blog entry, I will show you

  • why I finally fell in love with end-to-end testing of web application
  • how Cypress achieved exactly that and how to setup e2e testing with Cypress
  • a link to my sample code at Github, combining Cypress and Typescript
  • some more information about Continuous Integration

Background

I am a professional software developer now for almost 20 years, and I have run numerous successful projects of all sizes and in all possible branches, mostly big web applications.

Test-Driven-Development (TDD) is something I am really convinced of (more on that in later blog posts) but one thing was always painful: End-To-End Testing.

If you are coming from Selenium, you most likely have a similar mindset as I had:

Everything at E2E-tests is slow and cumbersome

Everything:

  • setting up everything locally
  • setting up everything on your CI server including Browser Support
  • developing tests
  • executing them
  • maintaining them
  • keeping track of false positives

Now, to be honest, Selenium was first released in 2004, and over the years it made a lot of improvements, but the web changed as well. Dynamic web applications built on Angular, React, or Vue on the one side, dozens of new Browsers and end devices on the other side.

Still, I'm coming from the Java corner, and that's why I always kept an eye on Selenium, also because the programming language was familiar to me. I'm not a big fan of recording E2E-tests, I'd rather like to code them as well, build reusable components, basically develop those tests like application code. Recorded tests are hard to maintain and while you may achieve fast results, you quickly end up in maintenance hell.

At Cloudflight, we're currently developing a quite huge mobile app and last year we've also implemented a test suite based on Selenium and Browserstack. We spent a lot of energy on getting it running, had contact with the really good support team at Browserstack as our APK was regularly crashing there, finally had a working test setup, also integrated with our CI-Pipeline, developed a couple of tests, and then it happened what happened with almost every E2E-Testing-Project I saw in the last years: The application continues to evolve, the tests not, developers get tired in maintaining the tests (or simply don't have time for it), false-positives frustrate everyone, and quite suddenly a dust layer builds up on those tests, and people don't care about it anymore.

Some months later I jumped into that project again, helping a bit before a big release. And as my frontend capabilities are really limited, I asked the team if it makes sense to get those Browserstack/Selenium tests running again. So, I've cloned the git repo, followed the readme, downloaded some 100 MBs of software, installed Webdriver, Android SDK, and Appium for local development. I also asked the team a bit to help me, and they were all rolling eyes, stating things like "yes, it's all not that easy". After some hours I had everything somehow running, tried to start to fix the failing tests, and it was just frustrating. The feedback loop was so long, finding out what exactly was the problem is the tests were - when you come from JUnit - so incredibly cumbersome, that after some hours I gave up.

Now frankly, I don't wanna blame either Selenium or Browserstack there. Those frameworks are mature and used by numerous projects all around the globe, the engineering teams behind those products are skilled and motivated and they have done great jobs (Selenium is even free to use). Probably we've also set it up wrong.

So I gave Cypress a try. Me, a Java developer for 20 years, now doing Javascript/Typescript E2E-testing. And what should I say? After 2 hours from scratch, I had a full test suite for the above-mentioned photobook designer. Technically, that was quite easily possible, as our app is built for cross-platform, having an Angular kernel, with Cordova for mobile and Electron.js for desktop around it, but there also exists a plain web version based on the same source base, so I can test almost all use cases also in the browser, including:

  • login
  • selecting products
  • uploading images
  • design a photobook
  • edit some settings like the title of the book
  • sending the order to the basket
  • performing checkout

Let me shortly share the basics about Cypress with you:

Create a cypress test project

The official cypress homepage already has great tutorials (starting with npm install cypress and very compact getting started tutorials) which I won't copy over to this blog, instead I'll use parts of our setup at Cloudflight that worked fine when working in teams.

All you need to do is to create a package.json with the following content (adapt name to your needs:

{
  "name": "cypress-hashnode-sample",
  "scripts": {
    "cy:open": "cypress open"
  },
  "devDependencies": {
    "cypress": "9.2.0"
  }
}

Then, run npm install and npm cy:open. Cypress will be downloaded on the first run (which may take a while), but then an application will open for you and you will see the following screen:

image.png

There is much to say here, and we will come back to that one later, for now, we'll notice two things:

  • Cypress has generated sample test files for us automatically
  • We can run all these tests immediately by clicking on the "Run 20 integration specs" link on the right side of the window.

So, let's go! A chrome window opens and you will see something like that:

image.png

There is so much great stuff in there:

  1. It just works
  2. Cypress executed 120 (!) end-to-end tests against example.cypress.io in only 76 seconds
  3. You can drill down into each test case into every single step and have a look at how the DOM and also the screen looked like at exactly that moment: image.png
  4. No installation of WebDriver or something similar
  5. You have live reloading of all test scripts
  6. It just works!

How does that work?

Cypress has the advantage that it is built entirely different from Selenium, as Cypress' founder Brian Mann explains nicely in this video:

On the Cypress homepage, there is also a detailed page how it works, and the first one mentions exactly that major advantage.

Most end-to-end testing tools are Selenium-based, which is why they all share the same problems. To make Cypress different, we built a new architecture from the ground up. Whereas Selenium executes remote commands through the network, Cypress runs in the same run-loop as your application.

And as Cypress runs together with your application inside the same loop you can gain all advantages that were not possible with webdriver-based solutions.

But there is much more to say, and I especially love their asynchronous support. Whoever has tried to automate e2e-tests on a single page application (i.e. Angular-based) knows how cumbersome it is to wait for client-side changes on the UI triggered by JavaScript. Thread.sleep() everywhere in your test cases - you don't need to do that anymore with Cypress.

Adding Typescript support

We don't wanna use the sample scripts now, instead we're gonna create our own simple scripts against this blog. But before that, we're gonna add Typescript support to our project. I told you, I'm coming from the Java world, that's why I'd

Adding Typescript is quite easy, just create a tsconfig.json in your project root:

{
  "compilerOptions": {
    "target": "es5",
    "lib": ["es5", "dom", "es2019"],
    "types": ["cypress", "node"],
    "esModuleInterop": true
  },
  "include": ["**/*.ts"]
}

and add Typescript to your package.json:

{
  "name": "cypress-hashnode-sample",
  "version": "1.0.0",
  "description": "sample application to test cypress against hashnode",
  "scripts": {
    "cy:open": "cypress open"
  },
  "devDependencies": {
    "cypress": "9.2.0",
    "typescript": "^4.5.4"
  }
}

That's it - we can now implement our specification files in a typesafe manner.

Adding the Cypress configuration file

Another important thing you should create right from the beginning is to create a Cypress configuration file called cypress.json in your root folder. You can use that file later for all kind of configuration properties, but for now we'll only add those two properties:

{
  "projectId": "cypress-hashnode-sample",
  "baseUrl": "https://agilecoding.io"
}

Create a test specification

Now it's time to create our first test, create cypress/integration/blog-tests.ts with the following content:

context('Blog tests', () => {
    it('Open homepage', () => {
        cy.visit('/');
    });
})

Your Cypress UI should have detected that new script and should look like that now:

image.png

If you've accidentally closed the UI, open it again by calling npm cy:open.

Now run the test, a special instance of Chrome will open and you will see something like that:

image.png

Now we're gonna add our first assertion to our specification:

context('Blog tests', () => {
    it('Open homepage', () => {
        cy.visit('/');
        cy.get('.blog-title').should('contain.text', "Agile Coding")
    });
})

The tests are being executed immediately (hot-reloading), and the result is:

image.png

Now let's do another test case, let's also input text and we're gonna search for an article:

    it("Search article", () => {
        cy.visit('/');
        cy.get('[data-title="Search"]').click();
        cy.get('input[placeholder*="Type"]').type("Hello");
        cy.get('input[placeholder*="Type"]').parent()
                  .siblings()
                  .children('a')
                  .first()
                  .click();
        cy.get('[data-query="post-title"]')
                  .should('have.text', 'Hello AgileCoding.io');
    })

This will click the search icon on the top right corner, enter the text "Hello", click the search, and clicking on the first anchor of the result list. After that, we're asserting that the title of the resulting page is "Hello AgileCoding.io".

Please note that the way how we're accessing the result list here using .siblings() and .children is not a best practice as this test then heavily depends on the DOM of the page. If you have access to the DOM of your page under test, then use data-attributes as decribed in this official guide.

Anyways we can't change the DOM of Hashnode, so let's run the test:

image.png

It passes, great. If you're interested in the whole project, check out my Github account.

CI Integration

Now that we've seen that locally on our development machines everything works well, you might ask yourself: and what about Continuous Integration? You might want to run your tests regularly on a central server and you know how hard it is to set that up with webdriver-based technologies Selenium.

Luckily, there exist two great options here with Cypress as well:

  1. The Cypress Dashboard: this is basically the business model of Cypress, it provides a hassle-free web platform where you can review all test runs including videos and screenshots without setting up your own infrastructure; it's a classical SaaS product, and it's really great.
  2. The open-source alternative Sorry Cypress which requires you to have your own infrastructure and a bit of patience to set everything up properly, but once you've done that, you can run all tests on your local CI server as well (we've done that at Cloudflight).
  3. Currents.dev which runs Sorry Cypress for you on a managed environment.

Summary

Seems I've found my holy grail of end-to-end testing for web applications. Cypress is really a game-changer and it even allows us to do Test-Driven-Development, meaning we are reproducing frontend bugs with Cypress before we fix them.

Cypress is really a game-changer, it allows us to integrate end-to-end testing into our daily routines.

There would be a lot to say about the details, but again: not only Cypress itself is great, but it also has awesome documentation and fantastic tutorials, so just dig into their resources.

Thanks, Cypress for finally making me smile when it comes to E2E.