A while ago, a friend of mine, who is just beginning to explore the wonderful world of frontend development, asked me how to start testing her application. Over the phone. I told her that, obviously, I can’t do it over the phone as there is so much to learn about this subject. I promised to send her links to guide her along the way.
And so I sat down on my computer and googled the subject. I found lots of links, which I sent her, but was dissatisfied with the depth of what they were discussing. I could not find a comprehensive guide — from the point of view of a frontend newbie — to testing frontend applications. I could not find a guide that discusses both the theory and the practice, and is oriented towards testing frontend application.
So I decided to write one. And this is the third part of this series of blogs. You can find part one here, part two here, and part three here. Also, for the purpose of this blog, I wrote a small app — Calculator — that I will use to demonstrate the various types of testing. You can see the source code here.
So we’ve seen the two parts of the spectrum of testing — unit testing your modules (or classes, functions, etc), and End to End (E2E) testing of…well, everything. But a lot of testing happens between these two endpoints. Myself, and lots of other people, tend to call them integration tests.
A word about nomenclature
Having talked a lot with TDD aficionados, I have come to understand that they have a different meaning for the term “integration tests”. From their point of view, an integration test checks the “outer boundaries” of their code, the code that interfaces with the outer-world.
So if their code uses Ajax, or localStorage, or IndexedDB, and thus that code can’t be unit-tested, they will wrap up the usage of those things in an interface, mock that interface when doing the unit tests, and test the real implementation of that interface with what is called an “integration test”. An “integration test”, from this point of view, just tests code that interfaces with the “real world” outside of those pure units that work without regards to the real world.
I, and others, tend to use “integration tests” to mean tests that check the integration of two or more units (again — modules, classes, etc). Whether you hide the real world via interfaces that you mock (or whether your don’t) is orthogonal (i.e. independent) to this.
My rule of thumb on whether to use real implementations of Ajax and other I/O in integration tests is this — if you can do it and the tests still run fast and are not flaky, then test again the real I/O. If the real I/O is difficult, slow, or flaky — then in the integration tests test using a fake/mock.
In our case, the calculator app, fortunately, the only real I/O is the DOM. There are no Ajax calls, so this question doesn’t arise.
Faking the DOM
This begs the question — do we need to fake the DOM in integration tests? Let’s try out my rule of thumb — will using a real DOM make the tests slower? Unfortunately, the answer is “yes” — using a real DOM means using the browser, and using the browser means, unfortunately, slow and flaky tests.
So either we somehow separate most of the code from the DOM, or we test most of everything in E2E tests? That’s not good. Fortunately, there’s a third solution: jsdom. This wonderful and amazing package does what it says — it’s an implementation of DOM in NodeJS.
It works, it’s fast, and it runs in Node. If you use it, you can stop treating the DOM as “I/O”. Which is incredibly important, because it is very difficult (I would say practically impossible) to separate the DOM from any frontend code. (I wouldn’t know how to do it.) I’m guessing that JSDom was written precisely for this reason — to enable running frontend tests under Node.
Let’s see how this works. As ususal — there’s the initialization code, and there’s the test code, but this time — we’ll look at the test code first. But first — an apology.
This part is the only part of the series that is framework-specific. And the framework that I chose to use is React. Not because it’s the best framework. I firmly believe that there is no such thing as a best framework. I don’t even believe, as others do, that there are best frameworks for specific use-cases. No — the only thing I believe in is that people should use the framework that they are most comfortable in, that they feel is the best for them.
And the framework I am most comfortable in is React, and so the following code is React code. But as we shall see, the solution of using jsdom in frontend integration tests should work across all modern frameworks.
OK, back to using jsdom.
The interesting lines are lines 10 to 14. In line 10 we render the CalculatorApp component, which (if you follow the code in the repository) also renders the Display and Keypad components.
Then, in line 12 and 14, we check that the element in the DOM that shows the calculator display has the (initial) value of 0.
And this code, that is running under Node, is using document! The first time I tried it, it was absolutely amazing. The global variable document is a browser variable, yet here it is, in NodeJS. A very large amount of code is needed to make those simple lines work. That very large amount of code, that sits in jsdom, is a mostly complete implementation of all that is in the browser, minus the rendering itself! (And I want to thank Domenic Denicola, Elijah Insua, and the other contributors to this package for their amazing work.)
Actually, line 10 itself (the line that calls ReactDom to render the component), also uses document (and window!), as ReactDom uses them extensively in its code.
So who’s creating those global variables? The test is — let’s look at the code:
In line 3 we are creating a simple document — just a simple div that we can “hang” our component on.
Line 4 is where we are creating a global window that accompanies the object. Why do we need it? We don’t, but React does, somewhere in its code.
The cleanup is just deleting those global variables so that they don’t waste memory.
Does this document and window have to be global? Because global is bad. Not only for theoretical reason, but also for practical ones — if they’re global, then we can’t run these tests in parallel with other integration tests (sorry, users of ava), because each would override the other’s global variables.
Well, unfortunately, yes, they have to be global — React and ReactDOM need document and window to be global — you can’t pass it to them. Maybe when React fiber comes out? Maybe. For now — we’re stuck with global document and window.
What about the rest of the test? Let’s look at that.
The rest of the test checks one scenario — tests the scenario where the user clicks “42*2=” and should get an “84”.
And it does it in the nicest manner possible — gets the elements using the well-known querySelector function, and then uses click to click on them. You can even create an event and dispatch it manually, using something like:
But the builtin click method works here, so we use that.
The astute among you may notice that this test tests exactly the same thing as the E2E test. True, but notice that this test is about 10 times faster, and is synchronous in nature. It’s much easier to write, and much easier to read.
But why, if the tests are the same, do we need the integration test? Well, because this is a toy project, and not a real one. Two components comprise the whole application, so integration and e2e tests do the same. But in a real application, an E2E test comprises hundreds of units, while integration tests comprise a few, maybe 10 units. So in a real application, there would be around 10 tests, but hundreds of integration tests.
It’s almost embarrassing how simple writing integration tests in Node is. Please — go forth and do this! And if you want to check out my ReactNext talk on this subject, here it is:
What did we see this week?
- We saw how simple it is to create a global document and window is, using jsdom.
- We saw how to test an app using jsdom.
- And that’s it. It as as simple as that.
This concludes the series about testing frontend code. If you feel I’ve missed anything, or have any other questions/comments/rants, please tweet to @giltayar, or respond to this article.
I must admit that I feel the itch to write one more post in this series — a post about testing applications that include Ajax calls, as any real application will have them.