Introduction to front-end testing. Part IV. Integration testing

SHARE

We considered two types of testing: unit testing of various modules and E2E testing of the entire application. But between these two stages of testing, there are others. I, like many others, call them integration tests.

A few words about terminology

Having talked a lot with test-driven development enthusiasts, I came to the conclusion that they have a different definition for the term “integration tests”. From their point of view, the integration test checks the “external” code, that is, the one that interfaces with the “outside world”, the world of the application.

So if their code uses Ajax or localStorage, or IndexedDB, and therefore can’t be tested with unit tests, they wrap that functionality in an interface and mock that interface for unit tests. Testing the actual implementation of the interface is called an “integration test”. From this point of view, an “integration test” simply tests code that interacts with the “real world” outside of those units that work without regard to the real world.

I, like many others, tend to use the term “integration tests” to refer to tests that test the integration of two or more units (modules, classes, etc.). It doesn’t matter if you’re hiding the real world through mocked interfaces.

My rule of thumb about whether or not to use real implementations of Ajax and other I/O operations in integration tests is this: if you can do it and the tests still run fast and are not flaky, then check I/O. If the I/O operation is complex, slow, or flaky, then 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: is it necessary to write fake DOM in integration tests? Let’s apply my rule. Will using real DOM make tests slow? Unfortunately, the answer is yes: using the real DOM means using the real browser, which makes the tests slow and unpredictable.

 

Will we separate most of the code from the DOM, or will we test everything together in E2E tests? Both options are not optimal. Luckily, there is a third solution: jsdom. This wonderful and amazing package does exactly what you expect from it – it implements the DOM in NodeJS.

It works, it’s fast, it runs in Node. If you use this tool, you can stop treating the DOM as “I/O”. And this is very important, because separating the DOM from the front-end code is difficult, if not impossible. (For example, I don’t know how to do this.) My guess is that jsdom was written specifically to run front-end tests under Node.

Let’s see how it works. As usual, there is initialization code and there is test code, but this time we will start with test code. But before that, an apology.

Apology

This part is the only part of the series that focuses on a specific framework. And the framework I chose is React. Not because it’s the best framework. I firmly believe that there is no such thing. I don’t even think there are better frameworks for specific use cases. The only thing I believe in is that people should use the environment they are most comfortable in, that they feel is the best for them.

And the framework I’m most comfortable with is React, so the following code is written in it. But as we will see, front-end integration tests using jsdom should work in all modern frameworks.

Let’s get back to using jsdom.

Using jsdom


const React = require('react')
const e = React.createElement
const ReactDom = require('react-dom')
const CalculatorApp = require('../../lib/calculator-app')
...
describe('calculator app component', function () {
...
  it('should work', function () {
    ReactDom.render(e(CalculatorApp), document.getElementById('container'))
    const displayElement = document.querySelector('.display')
    expect(displayElement.textContent).to.equal('0')

The interesting lines are 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.

We then check that in lines 12 and 14, the element in the DOM shows the initial value of 0 on the calculator’s display.

And this code, which runs under Node, uses document! The document global variable is a browser variable, but here it is in NodeJS. A very large amount of code is needed to make those simple lines work. This very large amount of code that is in jsdom is, in fact, the complete implementation of everything that is in the browser, minus the rendering itself!

 

Line 10, which calls ReactDom to render the component, also uses document (and window), since ReactDom often uses them in its code.

So, who creates these global variables? Test – let’s look at the code:


before(function () {
   global.document = jsdom(`<!doctype html><html><body><div id="container"/></div></body></html>`)
   global.window = document.defaultView
 })
 after(function () {
   delete global.window
   delete global.document
 })

In line 3, we are creating a simple document that only contains a div.

In line 4, we create a global window for the object. This is what React needs.

The cleanup function will remove these global variables so they don’t take up memory.

Ideally, the document and window variables should not be global. Otherwise, we won’t be able to run tests in parallel with other integration tests because they will all overwrite global variables.

Unfortunately, they have to be global – React and ReactDom need document and window to be exactly that, since you can’t pass it to them.

Handling events

What about the rest of the test? Let’s get a look:


ReactDom.render(e(CalculatorApp), document.getElementById('container'))

const displayElement = document.querySelector('.display')

expect(displayElement.textContent).to.equal('0')
const digit4Element = document.querySelector('.digit-4')
const digit2Element = document.querySelector('.digit-2')
const operatorMultiply = document.querySelector('.operator-multiply')
const operatorEquals = document.querySelector('.operator-equals')

digit4Element.click()
digit2Element.click()
operatorMultiply.click()
digit2Element.click()
operatorEquals.click()

expect(displayElement.textContent).to.equal('84')

The rest of the test tests a scenario where the user presses “42*2=” and should get “84”.

 

And it does it in a beautiful way – it gets the elements using the famous querySelector function and then uses click to click on them. You can even create an event and fire it manually using something like:


var ev = new Event("keyup", ...);
document.dispatchEvent(ev);

But the built-in click method works, so we use it.

So simple!

The astute will note that this test tests exactly the same thing as the E2E test. This is true, but note that this test is about 10 times faster and is synchronous in nature. It is much easier to write and much easier to read.

And why, if the tests are the same, do you need an integration one? Well, simply because it’s a toy project, not a real one. The two components make up the entire application, so integration and E2E tests do the same thing. But in a real application, an E2E test consists of hundreds of modules, while integration tests include several, maybe 10 modules. Thus, in a real application there will be about 10 E2E tests, but hundreds of integration tests.

Summary

What did we see in regards to all this?

  • 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.