Recently, I needed to test a button that would make an analytics tracking call using react-tracking and then navigate to a new page in a callback. This presented some challenges - I wasn't sure how to create a mocked version of react-tracking that would allow a callback to be passed.
A little context
This work took place in Volt, our partner CMS (it's sadly a private repository, but I'll do my best to paste in relevant code snippets so you're not totally in the dark). Volt has been around for a long time and has had several different client-side tracking implementations over the years. In this case, I wanted to take the opportunity to bring Volt up to standard with our other big apps, Force and Eigen. They both use react-tracking and Cohesion, our centralized analytics schema.
Our use-case was a button that would navigate the user to a new page. The button had been implemented in a previous PR, and now we wanted to make it execute a tracking call before navigating.
We use Segment for tracking, and their tracking setup relies on a JS snippet being available on your pages. That
snippet sets a
window.analytics property, which in turn
method. On a fundamental level, all of our tracking calls boil down to a call to
pass a list of properties to
.track(), Segment receives the event and properties, and the resulting JSON is
stored in our data warehouse.
First, there was a bit of setup required to get react-tracking working. The react-tracking package
assumes you're using Google Tag Manager by default,
but allows you to override that behavior with a custom
dispatch function. In our case, we wrap our React apps in
<BaseApp> component, so we added a new
<TrackerContextCohesion> component with a custom
dispatch that would
be available to all of our React apps:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
This allows us to make tracking calls in our components, including the passing of custom callback functions:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
Being able to pass a callback was especially important in our case. We realized that if we needed to track and
then navigate, the callback was necessary. In our testing, we saw that if we simply tried to fire the tracking
call then run
window.location.assign() synchronously, the tracking call might not get executed before the
navigation started, so we would effectively lose that event. Segment specifically
allows you to pass a callback
to their tracking function to their track function for this situation. They describe the optional
A function that is executed after a short timeout, giving the browser time to make outbound requests first.
Thus, we pass the tracking data and the callback to the custom
track call we implemented, and we're good to go.
The problem with testing
Our use-case is simple enough, but we wanted to make sure that when the button was pressed, we would both execute
the tracking call and then navigate. A test checking that the navigation worked had already been implemented.
However, after moving the
window.location.assign call into a callback, our test started failing because our
component was trying to execute a tracking call before navigating.
The test that predated the addition of tracking looked like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
So we were rendering our button, clicking on it, and expecting to try to navigate. How could we mock our tracking call while still executing a passed callback?
The final solution
Our mock ended up looking like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
Let's break that down section by section. First:
1 2 3
As noted above, all of our tracking calls assume
window.analytics exists and that it has a
.track() method. We
started by mocking that setup.
1 2 3 4
Here we mock the
react-tracking package and two specific methods it exports,
track. We made
useTracking a Jest function - we'll flesh it out further a few lines farther down in the file.
Then there's the mocking of
track. To put it in words, our mock is: a function that returns a function that takes
children and returns those
children. That might sound like gibberish at first blush, but essentially what
we're doing is mocking the function composition we performed earlier when creating
needed something that was the same shape as
track(), but we don't care about overriding
dispatch in our mocks.
1 2 3 4 5 6
trackEvent is a mock function that takes in an
args object and executes
args.callback(). We then update our
useTracking mock to make it return a function that returns an object with a
trackEvent property. What a
mouthful! That sounds super confusing, but remember that we're trying to mock something that we actually use like
So basically, our goal was to mock
trackEvent and we needed to emulate the shape it has when it's exported by
react-tracking. Hopefully that makes things a little clearer.
After some tinkering and eventually getting the mocks to work in a single test file, we moved these mocked
functions to a
setup.ts file that all of our Jest tests load automatically. We chose to make these mocks
available to all tests because then we wouldn't get surprising test failures if we, say, forgot that we were making
a tracking call in a component and didn't explicitly mock the tracking calls in those tests.
At the end of the day, we can use these mocked calls in our test files by doing the following:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
That's it! If you're trying to test something similar and found this post, I hope it helps you out. If so, or if you're still confused, leave a comment!