search
Part two of five

Testing your frontend code: Unit testing

Gil Tayar
frontend testing
© Shutterstock / REDPIXEL.PL

How do you start testing your apps? In the second installment in a series of five articles, JAX London speaker Gil Tayar introduces frontend testing for beginners, starting from what it is and why testing code is not optional.

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 second part of this series of blogs. You can find the other parts 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.

Unit testing

As we discussed in part one, unit testing is code that tests units of code, whether they be functions, modules, or classes. Most people believe that the bulk of the testing should be unit testing, but I don’t, but it’s OK if you do — as I will stress again and again during these blog posts, it doesn’t really matter how you do your tests, as long as you write enough tests that make you feel confident that you can deploy your version to production.

Regardless of how many unit tests you have, unit tests are the easiest tests to write, and the easiest tests to understand, as they are usually functional in nature — setup a unit with input, make it do it’s thing, and check the output (where the input could be a function parameter, and the output just the return value).

SEE MORE: 8 testing tools that every mobile app developer should use

Moreover, you should encourage yourself to write the code in a way that makes it possible to test those units in isolation, without needing to bring in other units.

Units in the calculator app

But enough of theory — let’s look at the Calculator app, whose source code can be found here. It’s a React application, which has two main components, the keypad and the display. These are definitely units, as they rely on no other units, but they are React units, so we will devote a future post to figuring out how to test them.

(If you’re looking into the code and wondering why I’m not use JSX, then the reason is that I didn’t want to get into transpiling. Both Node and all modern browsers recognize ES6 fully, so why shouldn’t I just run the code without transpiling? Yes, I know my code won’t run in IE, but this is demo code, so that’s OK. In a real project, I would transpile.)

But some code somewhere has to figure out what happens when clicking on a digit (e.g. ‘1’, ‘5’) or an operator (e.g. ‘+’, ‘=’). Which code is that? Well, as is customary today, I also divided my components into presentational components (keypad and display) and container components — calculator-app. This is the only component (in this application!) that has state, and it is the component that drives the logic of figuring out what the display should be whenever we click on a digit or operator.

The “calculator” module

But the code for figuring that out is not in the component! It is in a separate module, calculator, which has no React dependencies. This module is perfect to unit test! Code that is perfect for unit-testing is code that has no I/O or UI dependencies. You should strive to make the bulk of your application logic in such modules — modules that have no I/O or UI dependencies.

What does I/O mean in web applications? There are no files, databases, etc.? Well, no, but there are Ajax calls, local-storage, DOM access and manipulation, etc. Anything that touches a browser API is, for me, I/O.

How did I separate the calculator logic from the React component? In the case of calculator, it’s pretty easy. The logic is very algorithmic. I separated it into the module calculator.

The module is very simple — it takes the calculator state (an object), and a character (i.e. either a digit or an operator) and returns the new calculator state. If you’ve used Redux, this is similar to Redux’s reducer pattern (if you haven’t used Redux, that’s all right). But if you always get a new calculator state from the previous one, how do you get the very first one? Simple — the module also exports an initialState which you use to initialize the calculator. The calculator state is not opaque — it includes a field named display, which is what the calculator app needs to display for this state.

SEE MORE: Use automated testing to make sure your app is bug-free

If you don’t have the patience to look at the code, let’s see the beginning, which is the most important part, as the details of the algorithm are not that important:

module.exports.initialState = { display: '0', initial: true }

module.exports.nextState = (calculatorState, character) => {
  if (isDigit(character)) {
    return addDigit(calculatorState, character)
  } else if (isOperator(character)) {
    return addOperator(calculatorState, character)
  } else if (isEqualSign(character)) {
    return compute(calculatorState)
  } else {
    return calculatorState
  }
}

//.... 

The specifics of the algorithm are not that important, but what is important is that the function the module exports is very simple — given a state, we can always check what the next state is.

And this is what we do in test-calculator. This is where this logic, which is not trivial, is fully tested. And how do we test it? We always use a testing framework. The most popular framework is currently Mocha, and we use that. But feel free to use Jest, Jasmine, Tape, or any other testing framework you find out there that you like.

Testing a unit using Mocha

All testing frameworks are similar — you write the testing code in functions called test functions, and the testing framework runs them. The code that runs them is usually called a “runner”.

When npm install-ing Mocha, the mocha runner is a script called, unsurprisingly, “mocha”. If you look at the package.json, at the test script, you will see it there:

"scripts": {
...
    "test": "mocha 'test/**/test-*.js' && eslint test lib",
...
}, 

This will run all the tests in the test folder that start with the test- prefix. So that if you clone this repo, and npm install it, you can run npm test yourself to try it out.

SEE MORE: Integration testing using Spring Boot, Postgres and Docker

(BTW, the convention of having all tests in the test folder of the root of the package is a very strong convention in npm packages, so you should follow that if you want others to think you’re professional.)
When running it, the output will be something like:

frontend testing

Obviously, if a test doesn’t pass, you will see some angry reds there, which you will immediately run to fix, right?

Let’s look at the code:

const {describe, it} = require('mocha')
const {expect} = require('chai')
const calculator = require('../../lib/calculator')

describe('calculator', function () {
  const stream = (characters, calculatorState = calculator.initialState) =>
    !characters
      ? calculatorState
      : stream(characters.slice(1),
               calculator.nextState(calculatorState, characters[0]))

  it('should show initial display correctly', () => {
    expect(calculator.initialState.display).to.equal('0')
  })
  it('should replace 0 in initialState', () => {
    expect(stream('4').display).to.equal('4')
  })
//... 

We first import mocha, and it’s assertion library, expect (we’ll understand what an assertion library is in a minute). We just import the functions we need, which are describe, it, and expect.

SEE MORE: Testing Java Microservices: Not a big problem?

Then, because we’re testing it, we import the module we’re testing — calculator.

Then come the tests, which are described using the it function. For example,

it('should show initial display correctly', () => {
    expect(calculator.initialState.display).to.equal('0')
}) 

The it function accepts a string, which describes the test, and a function which is the test itself. But it tests cannot be “bare” — they need to be in test groups, which are defined using the describe function.

And what’s in the test function? Whatever we want. In this case, we test that the initial state’s display is equal to 0. How do we do that? We actually could have done something like this:

if (calculator.initialState.display !== '0')
  throw 'failed' 

This would have worked wonderfully! A test in Mocha fails if it throws an exception. It’s as simple as that. But expect makes it much nicer as it has lots of features that make testing stuff easier — things like checking that an array or object is equal to a specific value.

And that is the gist of unit testing — running a function or a set of functions (or instantiating an object, and calling some of its methods, if you’re into OOP), and checking the actual result against an expected result.

Writing testable units

Simple, right? What is complicated in unit tests is not the tests itself — it is the art of separating as much code as possible so that it is unit-testable. And code that is unit-testable is code that has little dependencies on other modules, and does no I/O. And this is difficult, because we tend to group logic code with I/O code and UI code. But it can be done, and there are lots of techniques to do it. For example, if you have code that validates fields, or groups of fields, make it a module with validation functions and test just that.

But the code runs under NodeJS!?

Notice an incredibly important thing here — the unit tests runs under NodeJS! While the calculator app itself runs in the browser, we’re using NodeJS to run the test, including the production code!

How can that be? Well, because our code is isomporphic. That means that it runs both under the browser and under NodeJS. How is that possible? Well, if you write your code so that it doesn’t use any I/O, that means that it doesn’t do anything browser-specific, then there’s no reason that it shouldn’t also run under NodeJS. Especially if it uses require, as require is recognized both natively by NodeJS, and by a bundler like Webpack. And if you look at the package.json, then you can see that we use Webpack exactly for that — to bundle code that uses require:

"scripts": {
   "build": "webpack && cp public/* dist",
   ...
} 

So our code uses require to import React and other modules in our code, and lo and behold, through the magic of NodeJS and Webpack, we can use this module system both from NodeJS and from the browser — NodeJS recognizes require natively, and Webpack uses the require system to bundle all the modules together into one big JS file, which is <script src>-ed into the HTML.

Running unit tests under the browser

By the way, we could use another testing framework, Karma, to run our Mocha code under the browser, but it is my humble opinion that if unit tests can run under Node (and today you can write code that runs under Node and under the browser very easily), then they should, as they are easier to run and debug. And if you don’t transpile, they’re really fast at execution!

But not running our tests under the browser is a problem, as we don’t reallyknow whether our code runs under the browser. There might be some minor inconsistency between JS in the browser and in NodeJS that breaks our code.

Next week

That’s what End to End tests are for — to test our code in the real environment: the browser. Tune in next week for how to write E2E tests.

Summary

What did we see this week?

  • We saw how to use Mocha (and Chai) to create a unit test.
  • We understood what a unit test is — code that tests one unit, that is independent of other modules.
  • We understood that we should keep our modules fairly independent of one another, but if there is a dependency, we should either mock the dependency and still use unit tests, or do integration tests.
  • And we understood that the units we test should be isomorphic code, so that it can be tested using NodeJS. And we understood how to build isomorphic code — no I/O, using require as the way to import modules, and using Webpack to bundle all modules for browser consumption.

 

This post was originally published on Hacker Noon.

Gil Tayar will be delivering a talk at JAX London that will go into more detail with these ideas, explaining how easy it is for you to build your own microservices architecture. He will also lead an introductory workshop on NodeJS.

all that we are, all that we need – they’re different things

Author

Gil Tayar

30 years of experience have not dulled the fascination Gil Tayar has with software development. From the olden days of DOS, to the contemporary world of Software Testing, Gil was, is, and always will be, a software developer. He has in the past co-founded WebCollage, survived the bubble collapse of 2000, and worked on various big cloudy projects at Wix.


Comments
comments powered by Disqus