Running unit tests in a browser environment

Délisson Junio
Aug 31, 2018 · 7 min read

I’ve recently investigated running tests inside a browser environment instead of the usual setup with JSDom. This lead to some interesting findings and a few gotchas.

The beginning: Jest in the browser

In the beginning, I tried to go the easy route: running jest in the browser. A quick google search revealed quite a few people trying to do the same. The linked Github issue for running Jest in the browser has a response from a Jest developer stating that running tests in the browser is not a priority for Facebook and thus, while they would accept external help for doing that, they won’t be implementing this themselves too soon.

In an attempt to see how hard it would be, I went to find where exactly the link between Jest and JSDom lies, and found this interesting piece of code:

class JSDOMEnvironment {
...

constructor(config: ProjectConfig, options) {
this.dom = new JSDOM(
'<!DOCTYPE html>',
Object.assign(
{
pretendToBeVisual: true,
runScripts: 'dangerously',
url: config.testURL,
virtualConsole: new VirtualConsole().sendTo(
options.console || console,
),
},
config.testEnvironmentOptions,
),
);
const global = (this.global = this.dom.window.document.defaultView);
...
}
runScript(script: Script): ?any {
if (this.dom) {
return this.dom.runVMScript(script);
}
return null;
}
}

Wait a second, this seems to be an awesome point where we could interface with the awesome puppeteer library to actually invoke a headless chrome instance and execute the given script, where it would have access to the browser’s environment, right?

Not so fast. The script parameter there is not a file, not a code string, not even a simple JS AST that could be sent to chrome... It's a node class from the vm module used to execute scripts in sandboxed environments. That is, we pretty much can't use this outside node (I haven't checked Electron, but this was supposed to be a quick investigation and I’d run out of RAM ¯\_(ツ)_/¯).

Given that there were quite a few people trying to achieve running Jest in the browser and not a clear choice to go yet, I decided that this path was probably too arduous for a quick project, so I tried another route.

I saw someone in that Github issue saying they had migrated to using Mocha for its support for in-browser testing, so I gave that a go.

Mocha (& Chai, & Mocha-Chrome, & Mocha-Headless-Chrome)

Mocha is pretty much the grandparent of testing frameworks for JavaScript, originally being quite a slim and simple library but over time being paired with tools such as Chai, Chai-as-Promised, Sinon, etc.

It has a quite expressive interface while being familiar enough and extensible enough to still feel the same even after being enhanced with these other tools. But that’s not why we’re after it, is it?

mocha-chrome and mocha-headless-chrome look like perfect fits for our task of running tests in the browser, so I tried to make our setup work with them. The first roadblock though was that both required an HTML file with all of the tests wrapped in a block such as

expect = chai.expect;// tests here
// literally
// describe('...', ...)
mocha.run();

I was able to, after a bit of work, leverage Webpack to build this file, gathering the test files (being included via require.context) previously used by Jest.

This brought some nice goodies, namely having tests auto-run on every Webpack build, having eslint automatically verify the test code as well and, the most important of all, the tests were built using the same infrastructure that builds the code.

This was super important, since the number two reason for investigating all of this is that Jest makes for an awkward setup with Webpack where we can’t use our original config files but the tests must still be able to import and interact with the rest of the code correctly.

There was an annoyance that Jest’s matchers and assertions are different from Chai’s, the latter being more expressive but, in my opinion, way more tedious to write and read. For example, this:

describe('the Password Input component', () => {
test('it renders a password type input by default', () => {
const wrapper = createWrapper(PasswordInput, getMountOptions())
const input = wrapper.find(ElementUI.Input)
expect(input.props().type).toBe('password')
})
)}

Being changed to:

describe('the Password Input component', () => {
it('renders a password type input by default', () => {
const wrapper = createWrapper(PasswordInput, getMountOptions())
const input = wrapper.find(Input)
expect(input.props().type).to.equal('password')
})
)}

The changes are rather simple in this case, but could get way more complex. And unlike Jest’s codemods, there seems to not be any simple library for converting Jest-style tests into Mocha/Chai tests.

These were small annoyances, so I kept going and finally had a version that could run a few tests on the browser, and that’s when I stopped for a bit and realised what exactly was achieved.

Actually running tests in the browser

mocha-headless-chrome worked just fine and the tests ran way quicker than I expected (many thanks to puppeteer!), but there were a few gotchas there:

  1. @vue/test-utils didn’t expose any option to control where exactly components would be mounted on the DOM, and would simply create a div in the document’s body and mount the component in there. This meant that I couldn’t style tests in a way to showcase each component’s output, and they would all just pile up in the browser.
  2. Again @vue/test-utils didn’t destroy components after their tests having finished. This was actually huge, as I’ll detail later. This is handled automatically in Jest by having the JSDom environment create a new document for every test suite, and due to the intrinsic sandboxing of test suites in Jest.

The point above also means that the browser’s DOM would grow linearly with the amount of tests. They were running pretty snappily, but we’ve only got a limited number of tests right now (< 100). This could quickly become out of control with a larger amount of tests. Especially since @vue/test-utils enforces Vue to be mostly synchronous, losing a lot of performance doing DOM changes instantly (instead of in batches).

These downsides were pretty heavy: running in the browser could work for now, but was a guarantee of headaches in the future. This all could be much smoother if the tests were ran using puppeteer directly and creating a new environment for each test, but that still doesn’t mean it’s all fine and dandy. We still wouldn’t have snapshot testing, for one.

Conclusion

All in all, when I actually got to running tests in the browser, I noticed some pitfalls that actually made it way worse than debugging a few issues here and there. I also realised that the Jest setup does bring quite a few benefits over Mocha, and most of our issues were actually with @vue/test-utils, which is still in Beta so I’d expect it to get way better as it matures.

I’ll conclude here that the grass is greener on our side: Jest has gained a lot of traction and has a lot going for it. In-browser testing is also just not a good idea as I once thought, and with some improvements we could benefit from many of the theoretical upsides of browser environments.

There are some things I’d like to do to bring some of the goodies from browser testing into our current setup, most of which I realised could be done without too big of a hassle:

  1. Improving our Jest setup to more closely match our Webpack build for code. This one is pretty big since it means that tests will be more consistent with code and even more consistent between environments. Jest seems to follow the philosophy of encapsulating building the code for us, so this might be a steep road (and one I’m not even sure we’ll want to get into), but it could pay off.
  2. Having tests running in real time alongside hot-reload is incredible. It really elevates tests’ status from “Ahh, just finished this feature, let’s write a few tests so that coverage doesn’t go down” to “I was writing this component but then had a bug that could creep in later. Better write a test for this while I’m at it”. As much as I believe a full TDD environment is more trouble than it’s worth, adopting part of it could be greatly beneficial.
  3. Have eslint check our test code as well. This is a must have and doesn’t seem to be too involved at all.
  4. Increase awareness of using Chrome’s DevTools debugger for inspecting tests. This is rather simple, but can make your life so much better when writing tests. Install the AWESOME Node.js V8 — inspector Manager (NiM) chrome extension and simply run your tests with Jest directly with node and specifying the debug flags: node --inspect --inspect-brk node_modules/jest/bin/jest.js. As soon as chrome gets focused again, you’ll see devtools opening targeting the node process, and will be able to add breakpoints, stop in debugger statements, inspect variables, etc. This could be big if we could inspect JSDom’s tree with the built-in inspector, but I wouldn’t wait around for that.
  5. I also noticed our Webpack setup is in need of some cleanup, especially now that Vue-CLI 3 brings its own configuration file — It could make transition easier or maintenance simpler if we do decide do stay with Webpack directly.

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade