Go Full Stack Rails: The Frontend Part 1

A brief introduction to test-driven frontend development

In “The Backend” part 1 and part 2, we mediated on the design of an endpoint to serve timeline tweets to a frontend client. In this post, we will resume our full-stack journey and start implementing the code to consume and present those tweets, and it will be written in ES6 JavaScript.

You may be curious about which framework we’ve chosen to power our application: is it React, Angular, Vue, Backbone, Ember, or another one? We must use a framework, right? It usually takes time to come up with an answer to this question, but I already have one: React is an excellent view framework and would be the choice of most modern JavaScripters out there, but let us take a different direction here.

It turns out that what we need to accomplish is quite simple, and given our requirements we don’t need the weight of a framework — it would probably be overkill! Furthermore, I don’t want to burden the reader with too many concepts at once — our focus shall be directed towards the JavaScript language and its raw power.

For this simple app, I find it much more valuable to use the organizational features provided by ES6 together with some small utility libraries. That said, we will come up with a simple and straightforward organization scheme and follow a few sensible conventions to guide our way.

By the way, have you ever wondered about which problems a framework like React solves for you? The best way to find out is to experiment and properly build apps without any frameworks. Chances are you won’t find their additional baggage and/or complexity worth in a lot of cases! Moreover, you will learn the essential technologies powering the JavaScript ecosystem.

Even though some frameworks force you to turn your app into an SPA, there is no need to do that without a strong reason involved in the decision — and with having Rails at our disposal, our app can become a healthy mixture of server-side rendered pages and JavaScript components. By properly breaking up our frontend logic we can achieve an abstraction much akin to a “component”.

For this part of the tutorial, I expect you to be acquainted with the basics of JavaScript and ES6. Now let’s move on and take a look at the aforementioned conventions.

Our coding conventions

I’ve come up with three simple conventions as part of this exercise:

Stick with simple functions unless a class makes sense

JavaScript is a different kind of object oriented language and its most powerful building block is the function. You will be surprised to acknowledge that we hardly need the shape of “classic” objects on this application. Our functions should aim to be highly specialized and strive to do one thing only.

export default function singlePurposeFunction() {
  # my code here
}

Use a bootstrap file to mount components

A bootstrap file is any file meant to initialize an application or part of its functionality, much like C’s “main” function.

Our Twitter Timeline component will provide a “mounter” function and readability will be its main selling feature: programmers who read this file will have a high-level overview of everything the component does, in just a few lines of code. The body will contain no logic whatsoever, just function calls and some glue code where needed.

export default function mountComponent() {
  executeStep1();
  executeStep2();

  bindSomeEvent();

  # other readable, high-level steps
}

We should never let this file grow out of bounds, for it would be a sign that the mounter is doing more than it’s supposed to do — which suggests the introduction of new abstractions.

Reference DOM nodes only within the bootstrap file

Functions should not hard code any DOM nodes; preferably, these nodes should come in as arguments of the mountComponent function — which in turn should delegate them down to other smaller functions.

The goal is to make code decoupled and reusable, which also provides nice side effects on tests. Small measures such as these are generally beneficial in the long run in order to keep a healthy system.

About JavaScript specs

The code42template application generator uses mocha as its JS test framework and karma + PhantomJS as its test runner.

You might recall from our first post that we generated the skeleton of our Twitter-based application using code42template. Check it out if you haven’t already!

PhantomJS is a headless browser; in our case it provides a full-fledged environment upon which to run integration specs. How does the mocha + PhantomJS combo work? The test environment starts out with a bare HTML document including the mocha.js script tag, a naked body tag and some code to trigger the execution of found spec examples. That said, it’s not a good idea to rely on any global state in your specs because the environment only boots up once.

First and foremost, let’s aim to write an integration spec for our Twitter-based app. JS integration specs are slightly similar to Capybara end-to-end specs, but they are isolated from the backend and thus run much faster and reliably than the former. What does that mean? It means our application is conceptually decoupled from the server and can be mounted anywhere, as long as a minimal skeleton template is provided in the HTML.

Before delving into our app, let’s learn how to write an integration spec.

The outline of a frontend integration spec

Every mocha spec follows an outline that is similar to the following:

  • Describe a subject using a describe function.

  • Declare shared setup variables at the top of the describe function.

  • Initialize shared setup using a beforeEach function.

  • Tear down shared setup using an afterEach function.

  • Have any number of examples in as many it functions as needed.

    The setup steps are optional. I recommend using them to share environment configuration required by all spec examples.

Show me some code!

Imagine we want to test a function that receives the price of some cart items and renders the total price on the screen. This function is naturally “integrated”, since it deals with two responsibilities and one of them is intrinsic to the browser environment: calculate a total price and render it out on the screen. For this reason, it’s a great candidate to be exercised by an integrated test.

What is the goal of this spec? We simply want to assert that the total price gets correctly displayed in a DOM node of our choice upon calling the subject function.

Without further ado, here’s the spec code:

import { expect } from 'chai';
import createFixture from '../support/createFixture';

describe('renderCartTotal', () => {
  // Declare shared environment variables
  let fixture;

  beforeEach(() => {
    // Setup the environment
    fixture = createFixture({ html: '<div data-js-cart-total></div>' });
  });

  afterEach(() => {
    // Teardown the environment
    fixture.remove();
  });

  it('calculates and displays the cart total', () => {
    // Setup
    const domNode = fixture.querySelector('[data-js-cart-total]');
    const itemPrices = [1, 2.5, 7.25];

    // Work
    renderCartTotal({ domNode, itemPrices });

    // Expectations
    expect(domNode.textContent).to.equal('$10.75');
  });
});

Three aspects are specially noteworthy here:

  • The shared setup scheme creates and destroys DOM nodes around every example. We call these special nodes “fixtures”.

  • createFixture is a small in-house function responsible for creating a fixture div tag into the DOM tree with any custom HTML we want.

  • We are using the chai BDD expect interface to obtain a syntax very similar to RSpec’s. It needs to be explicitly imported in every spec file.

    Watch out: the DOM is global! If you ever forget to clean it up, other specs onward may get in deep trouble! Removing the fixture node altogether provides a clean environment for subsequent specs to run upon.

Now let’s see what our spec looks like in action! First of all, we need to define a createFixture helper method to be used by the spec. Create the following file at the pointed out location:

// spec/javascripts/support/createFixture.js

function createFixtureNode() {
  const fixture = document.createElement('div');

  fixture.id = 'fixture';
  fixture.destroy = function destroy() {
    this.parentNode.removeChild(fixture);
  };

  return fixture;
}

export default function createFixture({ html }) {
  const fixture = createFixtureNode();
  const body = document.querySelector('body');

  fixture.innerHTML = html;
  body.insertBefore(fixture, body.firstChild);

  return fixture;
}

As you can see, this file holds no mystery; it uses the global document object provided by the browser environment to create and insert the fixture node into the DOM tree. It also defines a destroy function dynamically as a property of the fixture object, so that it’s easier to destroy the fixture at the teardown step.

Now copy the spec file over to spec/javascripts/integration/example.spec.js and run it:

$ npm run test:integration

The resulting output might look slightly polluted until you get used to it, but scroll up a little bit and you will see the following error message:

1) calculates and displays the cart total
 renderCartTotal
 Can’t find variable: renderCartTotal

That means we need to implement a renderCartTotal function. Write the following code at the top of the spec file, after the import statements:

import { expect } from 'chai';
import createFixture from '../support/createFixture';

function renderCartTotal({ domNode, itemPrices }) {
  const sum = itemPrices.reduce((total, price) => total + price, 0);
  const formattedPrice = $${sum};

  domNode.innerHTML = formattedPrice;
}

// Rest of the spec file here...

Now run the spec again and rejoice!

renderCartTotal
 ✓ calculates and displays the cart total

That’s awesome! We now have a working JavaScript spec, and despite its “integrated” nature it was quite easy to write!

Our first actual integration spec

It’s time to get back to our app and work on its first integration spec.

Before doing that, I’d like to introduce you to sinon: a popular mocking/stubbing swiss army-knife which comes baked into code42template. And not only that: the sinon project provides a very cool feature: the fake server. Let’s learn what it is and where we can use it.

The sinon fake server

Sinon Fake Server works by stubbing out the native XMLHttpRequest JavaScript APIs, so that it’s possible to simulate AJAX requests and make them return “canned” responses. Since every third-party HTTP client library must go through this lower level API, fake server should automatically work with all of them! Examples of third-parties are jQuery’s ajax method and the standalone axios library.

Using fake server is simple: provide the request and response details, trigger it out, and the canned response will kick in once a matching request gets performed anywhere in the application:

const httpStatusCode = 200;
const contentType = { 'Content-Type': 'application/json' };
const serverResponse = JSON.stringify({ foo: 'bar' });
const server = sinon.fakeServer.create();

server.respondWith('GET', '/my/endpoint', [
  httpStatusCode, contentType, serverResponse
]);

performRequest();

server.respond();

It’s worth noting that AJAX requests are asynchronous, and with sinon we can respond to them whenever we want.

On the example above we assume that performRequest() performs a non-blocking AJAX request behind the curtains, which means that while other code executes, the request keeps on waiting until a response arrives, then it handles the response asynchronously using a callback. When using sinon fake server, it’s mandatory to manually trigger the response using server.respond().

Onto the code

As noted in part 1, our application needs to render a default timeline upon initialization. The first step in that direction is to write a full integration spec describing part of the feature. How might we do that?

As you may have already guessed, we need to stub out the timeline endpoint developed in part 1 using the fake server. We’ll do that by simulating a response with two tweets and an “ok” status, and in the end we’ll assert whether the DOM has the right contents. Seems easy enough, huh?

For now, it makes a lot of sense for each tweet to have the following HTML structure:

Hey there!

There are other elements to a tweet such as “date” and “mentions”, but let’s not worry with them right now. Notwithstanding, our fake response will still contain all of them and we shall fill these gaps in the upcoming posts!

Hopefully, the following spec will look familiar to you:

// spec/javascripts/integration/twitter/mountComponent.spec.js

import { expect } from 'chai';
import sinon from 'sinon';
import createFixture from '../../support/createFixture';

describe('mountComponent', () => {
  let fixture;
  let server;

  beforeEach(() => {
    fixture = createFixture({ html: '<div data-js-tweets></div>' });
    server = sinon.fakeServer.create();
  });

  afterEach(() => {
    fixture.destroy();
    server.restore();
  });

  const serverResponse = JSON.stringify({
    status: 'ok',
    tweets: [
      {
        created_at: '2016-01-01T00:00:00.000-03:00',
        text: 'Hi @dude!',
        mentions: ['dude'],
      },
      {
        created_at: '2016-01-02T03:05:05.000-03:00',
        text: 'Pizza!',
        mentions: [],
      },
    ],
  });

  it('renders an initial timeline', (done) => {
    server.respondWith('GET', '/twitter_timeline/thiagoaraujos', [
      200, { 'Content-Type': 'application/json' }, serverResponse,
    ]);

    mountComponent({ containerNode: fixture });

    server.respond();

    setTimeout(() => {
      const tweets = fixture.querySelectorAll('.tweet > p');

      expect(tweets).to.have.length(2);
      expect(tweets[0].textContent).to.equal('Hi @dude!');
      expect(tweets[1].textContent).to.equal('Pizza!');

      done();
    }, 50);
  });
});

Basically, here’s what this spec does:

  • Injects the HTML fixture into the DOM tree.

  • Instantiates a fake server.

  • Simulates a request to the twitter_timeline/thiagoaraujos endpoint and makes it receive a fake response matching the format of the real backend.

  • Mounts the component: it’s assumed that mountComponent performs an AJAX request internally and the request’s callback uses the response to render tweets on the screen.

  • Triggers server.respond(), thus delivering the fake response over to the request’s callback.

  • Selects all tweets from the DOM using the global document object and runs expectations on their length and text.

But why are we using setTimeout? Good question! Do you recall that AJAX requests are asynchronous? So, that’s exactly the reason why we are going through this trouble. We must wait until the request’s bound callback receives and renders out the response.

Also, notice the done parameter at the it function’s signature… what does it even mean? Unfortunately, it returns before setTimeout starts running, but with done we can trick mocha into waiting for our signal. Its optional presence means: “hey mocha, there’s some asynchronous code in this example, but wait until I tell you it’s finished running!”. And by calling done() we are saying: “hey mocha, my async code is over and my assertions have been run, now go ahead and gather the spec results!”.

In the next posts we will refactor the code to not use setTimeout, which seems like a smell.

And what if we hadn’t call done()? In that case, the spec would have timed out after 2 seconds!

On the other hand, the complete absence of a done callback would have told mocha that results can be “gathered” as soon as the it function returns. Had we not used it, mocha would have assumed the example’s completion before any assertion had run, which means our spec would have erroneously passed with 0 expectations.

Let’s get back on track and run the spec with npm run test:integration. You’ll see the following error message after that:

Can’t find variable: mountComponent

It seems pretty obvious, let’s create a “naked” mountComponent function at the pointed out location:

// app/assets/javascripts/app/twitter/mountComponent.js

export default function mountComponent(opts) {
}

And finally, we need to import the mountComponent function into the spec. Add the following line at the top of the spec file, below the other import statements:

 import mountComponent from 'app/twitter/mountComponent';

After running the spec again a new kind of error pops up:

expected { length: 0, item: [Function: item] } to have a length of 2 but got 0

Awesome, that’s our first actual spec failure — it means there are no tweets rendered after running the code, which is correct! We can make it pass right away by introducing the following code in the mountComponent function:

export default function mountComponent(opts) {
  const containerNode = opts.containerNode;

  containerNode.innerHTML = `
    <div class="tweet"><p>Hi @dude!</p></div>
    <div class="tweet"><p>Pizza!</p></div>
  `;
}
mountComponent
    ✓ renders an initial timeline

What’s next?

OK, this is probably a silly way to make the spec pass, but at least it will remain green until we need to take the next big step, which is when we’ll get back to this code again! Most importantly, it forces us to reflect about what comes next:

  • Fetch timeline tweets from the server

  • Render tweets on the screen

How beautiful would our code look like if it had the following appearance?

fetchTimelineTweets(id).then(renderTweets);

The actual code won’t look exactly like this, but it seems like a great idea to try and improve upon! It’s beneficial to plant in ourselves the ability to imagine how code will look like beforehand; if it’s not clean or readable enough or if it does not fit into the requirements, it can probably be improved.

Wrap up… for now

This introduction focused a lot on the basics and hadn’t really a lot of code other than specs. Nevertheless, I hope you have enjoyed it!

In the next post we will continue our journey and work on smaller functions that’ll make our spec pass for real, and also introduce you to some libraries and modern JavaScripts concepts such as promises.

Until then, happy hacking!

If you have any questions or suggestions just hit me up in the comments!

Thanks to @talyssonoc for having a second eye on this post!

We want to work with you. Check out our "What We Do" section!