Running End-to-End Testing with Jest, Puppeteer, and Cucumber

Przemysław Paczoski
Docplanner Tech
Published in
9 min readMar 18, 2020

Docplanner is the world’s biggest healthcare platform. We operate on 4 continents where services are available in 14 countries. Our patients make over 2 million visits each month. Our application development is split into the marketplace and SaaS. In Poland, we focus mostly on the marketplace.

Becoming a big company with a mature application requires behaviors and solutions that reduce downtimes and the number of bugs. With the first part properly handled, as you can read in the article Happy helming at Docplanner — Docplanner Tech, we decided to look into the testing area.

Our company has a high culture with unit tests. We’re focused on the quality of work, development, and solutions but we are inexperienced in end-to-end tests. So we started with a simple question — ‘How to do them right?’.

We created a QA team, which focuses only on this area. They faced many barriers but delivered excellent solutions that I an about to show you below.

The context of this article is about the marketplace QA team based in the Warsaw office.

Testbase — stack

In the beginning, we decided to pick the same language as our developers — JavaScript. Going to stack, we had many options to consider: Cypress, Puppeteer, TestCafe, Protractor, etc.

Part of our team is in Barcelona, and they’re working on SaaS with Cypress as the main framework. After some debates about developing the marketplace tests we’ve selected:

  • Google’s developed solution — Puppeteer, which is a node library that provides a high-level API to control Chrome and Chromium browsers over the DevTools Protocol.
  • Facebook runner — Jest, which is a testing framework focused on simplification and giving a bunch of assets.
  • Cucumber — a tool that supports Behavior-Driven Development and allows us to write a logical language that non-technical people can understand.
  • Docker — a platform to create containers and bundle packages to run in different devices easily.

Launcher

When we began to build some base architecture and started to develop the first tests, we had to face some challenges:

  • Specific locales,
  • Part of all tests every run,
  • Clear browser cache every test run

Those issues gave us a headache, but finally, we found solutions for them.

To run the only particular of tests, we use tags:

  • [C] — core,
  • [O] — optional

Every test script is tagged, and after running the prepared command, we override the default JestJS test matcher and run only those tests we need.

The second problem was scripts that run only on mobile or desktop. Our application is made as an RWD — Responsive Web Design, so we have common code for mobile and desktop and some parts specific for one of the platforms. We did some brainstorming and ended up with a question — why not use a similar solution as with tags?

We created an environment variable, TYPE, which can take two different parameters:

mobile || desktop

We’re using this in command-line, for example,

process.env.DEVICE_TYPE=mobile

This solution, together with the tags mechanism, allows us to run one of seven types of tests:

  • Core or optional desktop-only
  • Core or optional mobile-only
  • Core or optional related to both types

This differentiation is possible by adding a specific tag to a test name:

  • [D] for desktop-only
  • [M] for mobile-only
  • No tag for no specified tests (can run in both types)

Examples:

  • [D][C] — core desktop-only
  • [C] — core related to both types

JestJS uses micromatch as a default matcher for its config. We used this to override default parameters and created a loader:

Many countries, many problems — localized config

Another problem was how to run tests only per locale. We are a global company, with specific sites and solutions developed for the target countries that depend on local business needs and local constraints.

Our solution was to create a config loader, from which a given environment variable will return translations specified to the locale.

We achieve this by creating a folder structure, where the name is connected to the parameter mentioned before. The scripts match the directory, then merge all files in and give us what we wanted. As an extra, we have additional config non-specified to the locale, where the things not related to the locale are stored.

Business-driven tests

Connecting tests with business is challenging. You have to tackle the such problems like speaking with managers, deciding which solution is best for the business, and developing it in a reasonable time. Our developers and testers are set in the business world on daily basis. That’s why we have chosen to write user stories.

Our test development process looks as follows:

  1. Talk with Product Manager about his product, core services, and requirements,
  2. Write user stories,
  3. Fill them with things essential for us,
  4. Show our work to PMs,
  5. Prioritize tasks — which is best to do first and why,
  6. Plan, develop, and enjoy our work!

We’re creating user stories with Gherkin syntax, which have three specific words for steps:

  • Given — describe the initial context of the system,
  • When — describe an event or action,
  • Then — describe an expected outcome or result.

We’re using a library called jest-cucumber that matches user stories with written tests. This connection allows us to implement precisely what we supposed to.

Reusable steps

If you’re writing many user stories, you can sometimes find duplicate steps, as we often do. Certainly, we can copy exactly the same code in every test and don’t care about it, but it’s not a solution.

The library that I mentioned before, jest-cucumber, has an implemented solution called shared-steps. We decided to use this.

With this solution, duplicated steps are moved to a specific file and only imported to tests. Now, instead of duplicating entire methods, we need to import only the file and use it.

Here is an example of one of our shared-steps:

Usage:

But what if you have to parametrize reusable steps? No problem! The given solution can be adjusted to the requirements that you have, even if it’s only a step’s name replacement.

Bullet-proof selectors

Creating tests for an application developed by almost 100 devs is hard. UI changes are on a day-to-day basis, and that requires a specific behavior and some agreements between us and dev teams.

One of our requirements was to stop deployment if an end-to-end test fails. Bringing it to work is challenging, and we still didn’t manage to find a perfect solution. But we have come to some conclusions.

First of all, after many debates and cups of coffee, we decided to develop a data-test-id.

It’s a simple HTML attribute without any influence on the UI. Its other benefit is the possibility of informing developers about using this element in the tests. So, if any breaking change appears, they have to maintain them.

Speaking of maintaining tests, we want to teach our developers what it looks like. The requirement of stopping the deployment cannot stop development — they need to know how to recreate a working test. Close cooperation also helps us deliver better solutions and create more maintainable architecture. What is more, they can learn something new, and it is always fun — right?

Recent steps

Being a team full of people craving knowledge, we started using TypeScript as the primary language. In turn, this has driven us to changes in architecture. Even if this challenge required some effort from us, it is clear now that it was worth it. The change in architecture was a second big step. We moved from POM — page object model — to a class-based architecture. Now, we have a structure similar to a frontend application development:

  • Views — that’s where we store a representation of specific pages with proper modules,
  • Modules — where parts of the page are being developed, like components.

Our listing view can serve as a nice example. It has modules like:

  • Navbar
  • Filers
  • Map
  • List of cards
  • Pagination

Every module has its own implementation with selectors and methods related only to this part. This solution helps us deal with a messed code in a file that contains about half a thousand lines of code. Also, each module is reusable, so if a developer adds, for example, a Navbar module to another page, then our work is only to import it ;).

Example of implementation:

Modules:

Views:

Test:

An approach like that gave us an opportunity not to duplicate methods and selectors. By creating such an architecture, we can move a method related to every module to the abstract class and initialize it with specific data.

For now, we’re thinking about a migration to the playwright.

Why are we a separate team?

You might consider why we are a separate team. We have a mature application with many features and core parts that have to work every time a user wants to use them. Creating a single team right now would require to make up for the time when the app was in the progress of its development and when the team didn’t exist yet.

The attitude we’ve chosen lets us quickly set up a team that can create a reliable architecture and can help us in the future. Also, we have some general sections to be covered with tests. We started developing them as soon as we created the needed basis.

As the next step, we considered splitting the team , so we might move each person to the product team once we got the core parts developed. Then, most of the work would be just maintaining and covering the feature parts.

Conclusion

In large, global applications, you have to face many problems. It’s requirements that determine the way you can work through them.

Just to conclude what I show you in brief points:

  • Testbase, stack — we’ve picked: Jest, Puppeteer, Cucumber, Docker,
  • Launcher — we’ve created a mechanism which overrides a default JestJS matcher,
  • Localized config — we’re using translations for locales through specific config loader,
  • Business-driven tests — to be close to business we’ve decided to create user stories in cooperation with PMs,
  • Reusable steps — avoiding a problem with duplicated steps, we’re using a shared-steps mechanism,
  • Bullet-proof selectors — we decided to develop a custom HTML attributes to use them in tests,
  • Recent steps we’ve changed the language and considered moving to another framework,
  • Why are we a separate team? — first, we need to cover architecture and core parts, and then we can switch our focus.

I show you the steps we took, how we structured code, and managed problems. I hope you get something from it and save yourself some time.

Good luck!

If you enjoyed this post, please hit the clap button below :) You can also follow us on Facebook, Twitter, and LinkedIn.

--

--