Back to Blog Home

Instrumenting Our Frontend Test Suite (...and fixing what we found)

Billy Vong image
Scott Cooper image

Billy Vong, Scott Cooper -

Instrumenting Our Frontend Test Suite (...and fixing what we found)

Adding Instrumentation to Jest

Here at Sentry, we like to dogfood our product as much as possible. Sometimes, it results in unusual applications of our product and sometimes these unusual applications pay off in a meaningful way. In this blog post, we'll examine one such case where we use the Sentry JavaScript SDK to instrument Jest (which runs our frontend test suite) and how we addressed the issues that we found.

We have high-level metrics for how well (or not) our CI is performing. This is useful to get a general sense of the health of the system, however, it does not help when doing a deeper dive into why a system is slow. We decided to instrument our CI jobs at the test runner level in order to get insights into the performance of the individual tests. We needed to be able to see if our test suite was slowing down because we were adding more tests or if it was because we had poor-performing tests.

As mentioned above, we use Jest as our test runner. It's important to note, that our instrumentation method requires using jest-circus test runner. This is the default for Jest version 27, however, it can be used with earlier versions of Jest. jest-circus is required because it allows us to listen to events from Jest by using a custom environment and defining a handleTestEvent method. Below is a basic test snippet annotated with an approximation of where jest-circus's events are fired.

// --> setup // --> add_hook<beforeEach> // --> start_describe_definition // --> add_hook<beforeAll> // --> add_hook<beforeEach> // --> add_hook<afterEach> // --> add_hook<afterAll> // --> add_test // --> finish_describe_definition // --> run_start describe('describe', function () { // --> run_describe_start // --> hook_start<beforeAll> // --> hook_success<beforeAll> beforeAll(function () {}); beforeEach(function () {}); afterEach(function () {}); afterAll(function () {}); // --> test_start // --> hook_start<beforeEach> // --> hook_success<beforeEach> it('test', function () { // --> test_fn_start expect(true).toBe(true); }); // --> test_fn_success // --> hook_start<afterEach> // --> hook_success<afterEach> // --> test_done // --> hook_start<afterAll> // --> hook_success<afterAll> }); // --> run_describe_finish // --> run_finish // --> teardown

Sentry's Performance Monitoring instrumentation is tree-like in structure with a transaction as the root node, and spans as child nodes. We can use a combination of the event name and the test name to determine if we should either create a transaction, create a child span from an existing transaction or end a span/transaction. Here's an example of how we implemented it.

Fixing Performance Issues

Starting an investigation into a slow Jest test is a bit like peering into a black box because there's so much abstraction. So the first steps are - find out which tests to look at and then find out what they're spending time doing. The slowest tests were all larger page views with many subcomponents and components that access data from our data stores. Bigger components should be slower, right?

Looking at the "alert rules index" test we can see that the beforeEach function is getting slower after every test, yet the code in the beforeEach block remains the same and should take just as much time on the first test as it does on the last test. Looking at what is in the beforeEach function wasn't enough to decide what was slowing it down. beforeEach contained a few api mocks and a call to ProjectsStore.loadInitialData([]); that was initializing the project  store with an empty array. But none of that should be slow, so let's have node tell us what is happening.

Start jest using the node debugger on just the one test file. The --inspect-brk flag tells node to wait until we have attached our profiler.

node --inspect-brk node_modules/.bin/jest --runInBand --no-cache tests/js/spec/views/alerts/rules/index.spec.jsx

After profiling the test and zooming into the area where the tests are running, we located the code that runs during the beforeEach. In this screenshot, we've renamed the anonymous beforeEach function to a named function badBeforeEach so it can be found easier. Below it is one call to ProjectsStore.loadInitialData followed by several calls to setState and a bunch of react work being kicked off. This shouldn't be happening as there shouldn't be a component listening to the store yet.

This led us to check that the component was being unmounted after each test. We added a componentWillUnmount to the class component being tested with a console.log inside to see if it was being called. When we did not see the console.log, we tested it manually by  unmounting the component after every test.

The result: our beforeEach function takes a fraction of a second every iteration instead of running change detection in components that should have been unmounted and our test has its overall total time cut nearly in half. The enzyme docs warn you that calling mount instead of shallowMount will persist components in the DOM and the reason we're seeing a larger impact on this test is because the store is still triggering change detection in components that should've been destroyed.

Using Sentry's Performance Trends feature, we can confirm the performance impact these changes made to the tests.

Instrumenting your own tests

We provided an example Jest environment that you can use if you're interested in instrumenting your own Jest tests. You'll need to install the jest-sentry-environment package and update your Jest configuration to use the fore-mentioned package, as well as supplying your Sentry DSN. For further instructions, please see the repository.

Share

Share on Twitter
Share on Bluesky
Share on HackerNews
Share on LinkedIn

Published

Sentry Sign Up CTA

Code breaks, fix it faster

Sign up for Sentry and monitor your application in minutes.

Try Sentry Free

Topics

Dogfooding Chronicles

New product releases and exclusive demos

Listen to the Syntax Podcast

Of course we sponsor a developer podcast. Check it out on your favorite listening platform.

Listen To Syntax
© 2024 • Sentry is a registered Trademark of Functional Software, Inc.