When it comes to front-end testing — or testing of other types of development, for that matter — my philosophy is to run unit tests as needed but to mainly focus on integration testing. I use Jest or Vitest with the React Testing Library to write my integration tests instead of using Cypress or other similar tools, and the tests work really well.
Integration tests allow you to check that different components are working together as expected, they require almost no mocking, and you don’t need to worry about testing implementation details. Plus, writing integration tests is almost like creating a simulation of a real user interaction, which means you can also test your user experience (UX) as well.
Despite all of those benefits, integration testing is not without its downsides. In my experience, the main issue has always been a lack of speed.
Weighing the options
In a way, it makes a lot of sense that integration testing speeds would be on the slow side. You need to render more elements when testing how they work together, and if you’re running hundreds or even thousands of tests, it can become time-consuming. Plus, if you like to write your projects in TypeScript, testing times can be even greater.
So what’s the solution?
One approach might be to make the scope of your tests smaller, but doing so can defeat the purpose of running integration tests in the first place. Another option is to mock certain components — even if they’re not required for a given test — so that you don’t have to wait for them to render. There are a few problems with this method, one of them being that maintaining the mocks when refactoring or adding features can be a nightmare.*
A third route to resolving the issue might be to try to optimize your app. Of course, if the app is working fine in production, then the slower test times aren’t due to a problem with your code. As a final option, you could also remove the TypeScript to avoid additional compilation, but for me, that’s a no-go.
*Full disclosure: I do mock some heavy external components from time to time, but only when I am sure it will give me real gains.
A simple solution
One day I found** a way to improve my testing speeds by 50% with very few changes to my tests or production code. While an improvement that significant sounds too good to be true, I promise that it isn’t, and the solution is even simpler than you might think.
There’s one thing that every front-end application has but is usually not needed in integration tests.
Can you guess what it is? I’ll give you a couple of minutes.
…
…
Styles!
Integration tests are not usually rendered in actual web browsers; instead, they use an in-memory library. The most common one is JSDOM, but there are some potentially faster alternatives like Happy-DOM.
When you use an in-memory library, you can’t see the rendered result, so to the user, it doesn’t really matter if background-color: red;
is applied to the element or not. However, even if you can’t see them, the rendering engine still needs to process all of your styles.
Style processing takes a lot of time, and given that you can’t view the rendered result, it’s time that isn’t well spent.
So the solution is simple: Turn off style calculations and enjoy faster tests.
**By found, I mean that the solution was suggested by David Ortner, the author of Happy-DOM library. I’m just giving the credit down here because I didn’t want to spoil the surprise!
The implementation
The only thing better than a simple solution is an easy implementation. All you need to do to turn off styles is to change one line of code in your test setup file:
window.getComputedStyle = () => ({ getPropertyValue: () => undefined }
With this one line of code, the computation of styles is completely disabled and you’re free to enjoy speedier testing.
The exception
Much like the solution itself, the implementation also may seem too good to be true. And this time, you caught me — there’s a catch! What if you actually want some of your styles to be computed? This may be the case in situations where you want to test the visibility of an element or a style used on it.
Luckily, there is a solution. For these scenarios, it’s possible to re-enable style computation for a single test. If you only have a few of these instances, it should be fast work. However, you can create some helpers to make it even easier.
For example, here is one of my setup files:
window.originalGetComputedStyle = window.getComputedStyle
window.lightGetComputedStyle = () => ({ getPropertyValue: () => undefined })
window.getComputedStyle = window.lightGetComputedStyle
beforeAll(() => {
window.getComputedStyle = window.lightGetComputedStyle
})
afterAll(() => {
window.getComputedStyle = window.lightGetComputedStyle
})
With some helper functions:
export function withRealStylesComputation() {
window.getComputedStyle = window.originalGetComputedStyle
}
export function withDummyStylesComputation() {
window.getComputedStyle = window.lightGetComputedStyle
}
export function resetToDummyStyleComputationForEachTest() {
beforeEach(() => {
withDummyStylesComputation()
})
afterEach(() => {
withDummyStylesComputation()=
})
}
Whenever I need to compute styles in a test, I can call the appropriate function and it computes them for me.
Now I will say something that I probably should’ve said at the beginning of the post. Of course, if I had, you might not have stuck around!
I learned the solution described in this post some time ago. Since then, libraries have improved their performance around computing styles. For example, JSDOM made some changes, but there isn’t a stable Jest release that has applied them, so you need to do it manually. Happy-DOM has also made some improvements.
Even so, removing style calculation is still an improvement over making code adjustments. Some work will never be as fast as no work, so I’m keeping these tricks in the codebase, regardless of library updates.
Remember: Just because you get a new hammer doesn’t mean you have to throw the old one away.