Testing DOM with Mocha, part 3

Doing DOM-level tests with Mocha & VueJS (part 3…ish)

Before reading this post, it might be helpful to check out my previous (or other) posts on the topic:

  1. Part 1 - Unit testing with JSPM, Mocha, and Vue.js
  2. Part 2 - More testing with JSPM, Mocha, and Vue.js
  3. Part 3 - Testing the DOM
  4. Part 4 - Switching from mocha to tape

BONUS Want to see it all working? Check out the gitlab repo with a fully-built example

This post is serving as a part 3 of this series, exploring how I’ve started integrating close-to-DOM level testing in my components.

tldr; use vue-server-renderer to easily (and quickly) test static component rendering in a just-outside-dom context. Any component that renders correctly on the client should at least render properly on the server (especially if you’re aiming for an isomorphic app).

A word on end-to-end testing

“End-to-end”/“Acceptance” tests are the most important tests of any user-facing application, as far as I’m concerned. It doesn’t matter how many of your unit/integration tests pass, if someone can’t use your UI to make anything happen. Also, by virtue of a test being E2E, it necessarily tests the backend as well, in the ultimate black-box and true-to-life sense of the concept. The most important parts of your app should have at least one E2E test each (especially if that action is related to you getting paid).

The question of “how much to test” is also a very important consideration. Do you need to test that a library like jQuery is doing it’s job? Probably not. Do you need to test that VueJS is properly making reactivity changes you expect? Maybe (some might say definitely, some might say no).

The simplest case: rendering with vue-server-renderer

More indepth documentation can be found in the vue-server-renderer documentation, but here’s the code, roughly:

var renderer = require('vue-server-renderer').createRenderer();

...

// Render the component
    renderer.renderToString(vm, function(err, html) {
    if (err) throw err;

    // Check generated HTML here
});

Benefits - Easily test basic HTML issues (like for example, most components should probably not generate empty HTML) - Running a test with this logic inside is very fast

Downsides - This doesn’t really get enough of a browser’s context to be considered a e2e test by my standards.

Adding JSDOM to increase the E2E-ness of the test

Wanting a bit of a closer approximation the browser context, I considered adding selenium or maybe phantom, or maybe even switching to another test runner like Karma but they all just seemed far too heavy. I wanted something that was ALMOST an actual browser DOM, but just enough to different that I didn’t have to deal with unwieldly APIs and a steep learning curve. jsdom fit the bill perfectly. Maybe I’ll revisit this in the future as karma seems to be industry standard (and I assume it’s so for a reason), but I just can’t take the time to try and grok all of what it does at the moment.

After this, the test looks somewhat like this (raw):


... inside the test...

// Render the component
renderer.renderToString(vm, function(err, html) {
  if (err) throw err;

  // Start up JSDOM and check the element
  jsdom.env(html, function(err, window) {
    if (err) throw err;

    // Get element
    var elem = window.document.getElementsByClassName("alert-notification-component")[0];

    var content = elem.textContent.trim();

    content.should.not.be.empty();
    content.should.containEql('×'); // × in the template
    content.should.containEql(TEST_ALERT_ERROR.message);

    done();
  });

});

As you can see, this really just takes the HTML that was generated by the vue-server-renderer and puts it on a jsdom-managed page. Then I can use DOM interfaces like window and document to do testing that resembles what a in-browser test would look like.

After some refactoring, the whole tests ends up looking like:

describe("rendering", function() {

  it("properly renders a single alert", (done) => {

    TestUtil.renderComponentWithData(Vue, Component, INIT_FIXTURE_TEST_ERROR)
      .then(TestUtil.getComponentElementByClass("alert-notification-component"))
      .then(e => {
        var content = e.textContent.trim();

        content.should.not.be.empty();
        content.should.containEql('×'); // × in the component for closing the notification
        content.should.containEql(INIT_FIXTURE_TEST_ERROR.propsData.alert.message);

        done();
      })
      .catch(done);
  });

});

To briefly explain the refactoring that has happened, I’ve added a method that does all the requisite setup, and returns a promise that resolves to the window object provided by JSPM. I’ve also added a helper function getComponentElementByClass which takes the window object, gets the element specified, and returns JUST the element.

The rest of the test looks identical to what was there before.

Lots of twiddling/hacking

Again, this approach (vue-server-renderer + jsdom) seemed to only be cable of doing very basic DOM testing, thisgs like clicking buttons, etc didn’t work properly, for example. It also added quite a bit of runtime to each test, roughly 500ms.

At this point, I looked again at Karma, just to make sure this complexity was still something I could stomach vs just jumping in and learning Karma. Once again I felt that Karma was still too heavy and just not worth the cost for me yet. I felt that I had wrangled the present complexity well enough that Karma would still be adding significantly more overhead. There was also this significant feeling that all the pieces were right in front of me, all I needed to do was to put them together in the right way.

In particular, when hacking around jsdom, I tried: - Loading the component in a script tag then rendering it to the page - Attempting to load the files in SystemJS and vue independently and pass them through to jsdom with the src option - Using JSPM to build an in-memory bundle of the file (this worked wonderfully and is part of the approach I use now)

NOTE - This problem was exacerbated by the fact that my modules are written in ES2016, this meant I NEEDED transpilation, even for in-memory builds (can’t just load the code with nodejs’s require()).

Adding even more E2E-ness, Bringing JSPM/SystemJS into the mix

After lots of twiddling and hacking, I realized that using jspm to build an in-memory bundle and load THAT on to the page was a great way to test every component – it made sure their dependencies were satisfied (as is the job of jspm), and made it easy for me to actually load a html fragment that USED what I knew was being imported.

After looking through the jspm documentation for how to do something like an in-memory bundle as well as the systemjs documentation on builders, I hit on a winning process. It goes something like this:

  1. Initialize a SystemJS Builder
  2. Rather than doing this manually (via script tags or something), use jspm’s node module and API
  3. Build a static version of the component, by building a static bundle starting with the component file
  4. Render an empty html fragment with jsdom.env
  5. Source the (in-memory, generated) bundle
  6. Ensure that the loaded module/bundle exposes the Vue instance used to register the module and the component object (actual functionality) in the component file

NOTE #6 This is more specific to how I have the components set up, you can technically define components as JUST the object and register them later, you don’t necessarily need to import “vue” when defining components

  1. Once the page is loaded, grab the component and Vue, and create an instance of that component
  2. Do whatever testing needs to be done

NOTE Note that vue doesn’t respond to just <HTMLElement>.click() function. You need to dispatch a mouse event from the correct component (see code).

Here’s the code, before refactoring:

describe("interaction", function() {
  this.timeout(10000);

  it("should $emit a destroy event when the x is clicked", function(done) {

  // Get an instance of the JSPM builder, get a source for the component
  var builder = new JSPM.Builder();

  // Build the minimal bundle for the component
  builder.buildStatic(COMPONENT_JS_PATH, { minify: true, globalName: 'Module'})
    .then(output => {

      // Render the element completely: Load SystemJS, use it to load vue and the component
      jsdom.env({
        html: '',
        src:  [output.source],
        done: function(err, window) {
          if (err) throw err;

          // Grab the loaded element, component, and vue fn
          var vueFn  = window.Module.default.vue;
          var comp  = vueFn.extend(window.Module.default.component);

          // Attempt to manually mount component into the element
          var compInstance = new comp(INIT_FIXTURE_TEST_ERROR);
          // Create off-page element
          var vm = compInstance.$mount();
          var elem = vm.$el;

          // Watch for destroy to be called
          vm.$on('destroy', function(alert, alertIdx) {

            // ensure that the $emit-ed destroy contains the alert and alert ID we expect
            alert.should.be.deepEqual(INIT_FIXTURE_TEST_ERROR.propsData.alert);
            alertIdx.should.be.eql(INIT_FIXTURE_TEST_ERROR.propsData.alertIdx);

            done();
          });

          // Click on the X, ensure it triggers a destroy,
          elem.firstChild.dispatchEvent(
            new window.MouseEvent("click", { bubbles: true, cancelable: true, view: elem })
          );

        }
    });

  })
  .catch(done);

  });

});

After some refactoring:

describe("interaction", function() {
  this.timeout(10000);

  it("should $emit a destroy event when the x is clicked", function(done) {

    // Render module in JSDOM
    TestUtil.renderModuleInJSDOM(COMPONENT_JS_PATH, "Component")
      .then(TestUtil.instantiateComponentFromLoadedGlobal("Component", INIT_FIXTURE_TEST_ERROR))
      .then(elemInfo => {

        // Watch for destroy to be called
        elemInfo.vm.$on('destroy', function(alert, alertIdx) {

          // ensure that the $emit-ed destroy contains the alert and alert ID we expect
          alert.should.be.deepEqual(INIT_FIXTURE_TEST_ERROR.propsData.alert);
          alertIdx.should.be.eql(INIT_FIXTURE_TEST_ERROR.propsData.alertIdx);

          done();
        });

        // Click on the X, ensure it triggers a destroy,
        TestUtil.jsdomClick(elemInfo.elem.firstChild, elemInfo.window);
      })
      .catch(done);

  });

});

As you can see, I’ve factored out the tedious logic into some readable functions that return promises and make it easier to isolate just the important parts of the tests.

Overall, things are great, except…

So this approach works great, and gets me very close to full-featured browser testing with less cost (in complexity, and runtime, JSDOM is quite bit faster).

The issues however, seems to be with mocha. The following things perplexed me: - The inability to simply set a pre-file setup – setup() block seemed to be global, not tied to files, and didn’t run before describes at the same level (only it). This made my otherwise very readable test suite awkward. - A possible solution here was to use --delay, but this felt like just a hack - Why does mocha create an invisible root suite? That smells like bad design to me. - https://github.com/mochajs/mocha/issues/2257 - https://github.com/mochajs/mocha/issues/362 - All I want is to run set up for the tests in the file, then run the tests in the file. Why is it taking more than 2 mins to figure out how best to do that with mocha?

I was perplexed at these things, and I think the relative ease of mocha everywhere else actually compounded my frustration… mocha is such a simple tool that normally does the right thing, why is it so frustratingly difficult about this one point?

I started looking around, and of course, I found an even simpler library, tape. It was introduced to me by way of a blog post explaining someone else’s switch from mocha.

The only obvious problem with tape right off the bat was that the way to extend it is to wrap it (there is no plugin system built into tape, and one will not be added, because of complexity). Of course, extending things in this way makes for shitty (nonexistent) composability. I raised an issue, which was responded to faily quickly, and was reassured that part of tapes value was this simplicity, which is why this wasn’t being pursued.

Hopefully someone someday (me? maybe) might take on the complexity of creating composable wrappers for tape, so someone can wrap tape in more than one way (my issue was the inability to wrap more than one functionality into tape at a time, quality of wrapper code aside).