If you want to adhere to high code-quality standards, testing your front end is as important as testing every other part of your system. And while it’s not always easy to do, automating these tests is crucial; otherwise, people will get weary and stop doing them.
The next logical step, after automating your tests, is to integrate them into your CI/CD pipeline so that all of your new versions are tested before release. While simple unit tests are easily integrated into a CI/CD pipeline, it is inevitable these tests become more complex and it gets harder to keep track of the pipeline.
Fixing E2E tests when they fail is infamously difficult. They are brittle, flaky, and usually the most long-running tests; also, they can often take hours to run. If you test multiple parts of your system at once, which is the case with integration and E2E tests, it becomes incredibly challenging to locate the code lines that caused them.
So, knowing this, let’s look into some tips and tricks you can apply so you don’t run into such problems in the first place. And if you do, we’ll also show you how to find them quickly and fix them.
Using Static Typing and Code Linting
In the spirit of the shift-left movement, try to catch your bugs as early in your development pipeline as possible.
The best place to start is right in your head: Think hard before introducing new code into the system—every line is a potential source of errors.
But since the software is created by writing code, the next best thing is to get help when writing that code. For this, you should look into static typing with languages like TypeScript or tools like Flow.
To get a better grasp of the mindset behind a component you use, static typing can help. If everyone defines an interface for their components, then the users of those components see what they have to do without having to guess. Often, you can even see the errors in your IDE while writing the code and can fix them before you run the application, not to mention before pushing your changes to the CI/CD pipeline.
Striving for Integration Tests
Integration tests are the middle ground between unit tests, which test a singular part of your application (like one class or one function), and E2E tests, which test complete use cases (like signups, which might touch every part of your application).
Unit tests are beneficial because when they fail, it’s easy to figure out why. But you have to write many of them, and you can’t even be sure that multiple components will work well together because they’re all isolated from each other.
E2E tests are beneficial because they really test your application as it is. This allows you to write one test that can potentially catch multiple bugs, but finding those bugs isn’t always easy; after all, they could be everywhere. Also, since they can run through all the layers of your architecture, they take much longer to execute than unit tests with databases and network requests. You could use TestCafe for managing these complex tasks.
Fixing integration tests is usually easier than fixing E2E tests because they are more isolated than E2E tests and still give you the benefit of finding bugs that occur where your components interface with each other—which is usually where the most problems arise.
Automating Test Data Provisioning
As mentioned before, automation is crucial when you want to keep people running your tests. This is also true for the provisioning of the data that these tests operate on. If you have to click around in multiple UIs to get the baseline set up before you can even start the tests, you just have more obstacles to get around before running a test. This only worsens when tests fail, and developers have to set up everything again and again until a bug is fixed.
Instead, write scripts that set up everything needed for your tests. Then add the execution of these scripts as a step before your tests in your CI/CD pipeline.
The more boring a task is, the better it’s done by a script that can be triggered automatically.
Catching Unexpected UI Changes with Snapshots
Snapshot testing is an easy way to catch unintended UI changes quickly. They render the UI as an object representation, which can be done without a browser, and then save it to disk. These are like screenshots, but for a UI structure. There are tools like Puppeteer to do the task for you. When you integrate it to your CI/CD pipeline, you could observe how your UI is changing at each commit.
In later runs, the new snapshots are compared with the saved ones to confirm that they didn’t change by accident. And when a developer deliberately changes part of the UI, they update the related saved snapshot.
Testing Complex Components on Their Own
Integration tests are the sweet spot of frontend tests, but the more complex a UI component gets, the better off you are testing it on its own. A simple navigation bar might be tested well alongside other components in integration or E2E tests. But if you think about software like Photoshop or Final Cut Pro, you might realize that things aren’t always that straightforward.
If your component goes beyond a certain complexity level, treat it to its own unit tests so you can locate problems quickly and don’t have to wait for a two-hour E2E test to finish before you notice something went wrong.
Using Data Attributes in your E2E Tests
If you write E2E tests, you shouldn’t use CSS selectors. They can be used to select UI elements, but they are really tools used primarily by designers. Often HTML structures and CSS classes and IDs vary with even simple UI changes.
Custom HTML data attributes, created just for testing, are the way to go. The rest of the frontend team, the designers and developers, can mostly ignore them when doing their work, and you can be sure you’ll find a button, even if it moved from one part of the UI to another. They make debugging E2E tests much easier later on.
Waiting for Events with Reasonable Timeouts
If you’re running E2E tests, you’ll quickly end up waiting for database or network requests. Write your tests so that they wait for an event to finish—don’t set it up to wait for 10 seconds just to be safe. This way, if a request gets faster because of better hardware or such, your test gets faster too, without you changing anything.
But try to keep track of how long such a test took in the past. If your timeouts are too long, you might be wasting time waiting for errors that happened quickly. This is also a good way to catch performance regressions. They might not be bugs in the classical sense, but your users might notice something getting slower with a new release.
Monitoring Your Tests Inside the CI/CD Pipeline
Even if you follow all the tips in this article, you will still encounter bugs. This is just the way things are with software development. The next best thing besides preventing bugs is to fix them quickly.
E2E tests are notorious for throwing bugs at you that are hard to locate. That’s why you should use an observability tool like Thundra Foresight to improve CI test visibility. If you instrument your testing code with Thundra, you will get valuable insights and find test failures fast. Look at the logs to find out what went wrong or investigate the automatically generated tracing graphs to see which service could be the culprit for your error.
This way, you can see with just a glance how the click of a button changed your database.
Testing is a crucial practice to maintain software quality. But just dumping hundreds of unit tests on your codebase is usually a waste of time. Try to use static typing or linters to solve problems before they are committed to a repository. Keep your tests structured in a meaningful way so you can quickly navigate to tests that failed.
Go for the middle ground with integration tests. The exciting parts of your system are usually the interfaces between your components, so integration tests give you the most bang for your buck.
And if things still go wrong, Thundra has you covered with its new test monitoring tool, Foresight. It provides testing logs and automatically generated traces that show you the flow of events across your entire system to find failures fast.