Spaceships and testing in Javascript

What do a Spaceship and JavaScript have in common? Both already reached space.

The Crew Dragon Spaceship from SpaceX uses JavaScript in the main cockpit panels[1]. It’s super cool to see where the language has come from and what can be achieved with it.

Just like rockets, spaceships, and many others, critical and non-critical projects, require a lot of testing before production launch. Otherwise, a “KaBuM! Effect” could happen, and unless it is a firework, it won’t make anyone happy.

In any case, testing is not complicated, and even if most of us are not building things that can explode, treat them as if they are of equal importance. Testing makes error detection easier and can also save a lot of time. It can be tricky at first, but with practice and experience, it becomes an ally, you just need to make it part of your daily work.

Before getting started, we need to take a look and understand how things work under the hood. This article will cover the basics of testing using JavaScript, including:

  1. Testing Fundamentals
  2. Testing with Jest
  3. Mocking Fundamentals
  4. Static Code Analysis

Testing Fundamentals

One of the most common phrases in software development is: “whattaf*ck…”, some say that the quality of the code can be measured by the FPS (f*cks per second) heard during the development process. $h!t happens, and fixing it can be simple, but if it is a little more complicated, it can take days, weeks, and even months to solve it. Thus, the idea of creating an automated test is to try to catch as many errors as possible in our code before they happened.

Imagine that you are building a spaceship, and this spaceship requires a calculator module and if it fails it can explode. You aim to make sure the results are always correct. Then, you start creating the first method of this module.

export const sum = (a, b) => a + b;

Show code in action

To test this code, you have to check the result of your function to validate your assumption.

import { sum } from './calculator.js';

const expected = 4;
const result = sum(2, 2);

if (result !== expected) {
  throw new Error(`KaBuM! It Exploded!`, { cause: `${result} is not equal to ${expected}` });

Show code in action

In the example, you run and test to check if the result is what you expected. Although this implementation works, it cannot be reused. To simplify the testing process, extract the logic into a new method, that way it can now be used for more cases.

export const expect = value => ({
  toEqual(expected) {
    if (value !== expected) {
      throw new Error(`KaBuM! It Exploded!`, { cause: `${value} is not equal to ${expected}` });
    }
  }
});

Show code in action

Now, update our previous code.

import { sum } from './calculator.js';
import { expect } from './testing.js';

expect(sum(2, 2)).toEqual(4);
expect(sum(2, 'a')).toEqual(NaN); // Error

Show code in action

Much better! However, there is no description showing what is being tested, and if you start adding more tests and one fail, the remaining tests will not run, so let’s fix it by encapsulating this code inside a try/catch:

export const test = (description, fn) => {
  try {
    fn();
    console.log(`✓ ${description}`);
  } catch (error) {
    console.error(`✕ ${description}`);
    console.error(error);
  }
};
// ...

Show code in action

Now, use our new function inside our test code.

import { sum } from './calculator.js';
import { expect, test } from './testing.js';

test('sum numbers', () => expect(sum(2, 2)).toEqual(4));

Show code in action

Let’s open the terminal and run our test:

$ npx babel-node calculator.test.js

In case of an error in the code, you will see the following error message.

...
test('sum numbers', () => expect(sum(1, 2)).toEqual(4));

Show code in action

Congratulations! You have now created a simple JavaScript Testing Framework. The good news is that there are already some great tools for testing automation. The most famous is Jest, and you can make your test compatible with it by just removing one line of code and running it:

import { sum } from './calculator.js';
test('sum numbers', () => expect(sum(2, 2)).toEqual(4));

Show code in action

In the terminal, run the command:

$ npx jest calculator.test.js

There is more than just that. Let’s move on and learn more about how to test an application, even without running the code (Yep, this is possible in JavaScript).

Testing with Jest

Jest is a delightful JavaScript Testing Framework with a focus on simplicity. It has already built in most of the features you expect from a testing framework: A great set of exceptions, code coverage, mocking, runs fast, good documentation, and an incredible community around it.

First things first, you need to install Jest

$ npm install --save-dev jest

Thereafter, update the package.json file to run it:

...
"scripts": {
  "test": "jest"
},
...

Show code in action

npm run test or also manually: npx jest

If you like, you can also run jest --init to create a configuration file

Comparing Values

Let’s go ahead, now you are going to create a weapon module for your spaceship. Start creating a simple test example:

describe('Weapon Module', () => {
  test('a simple test', () => {
    expect(2 + 2).toBe(4);
  });
  // ...
});

Show code in action

After running Jest, this is the result:

The primary comparison methods are toBe and toEqualtoBe uses === to check strict equality, while toEqual makes a deep comparison of the properties of the values using Object.is.

// ...
const weapon = { type: 'laser' };
test('check object with toEqual', () => {
  expect(weapon).toEqual({ type: 'laser' });
});
test('check object with toBe', () => {
  expect(weapon).toBe({ type: 'laser' });
});
// ...

Show code in action

Running this test, you will get the following result:

It is up to you to decide which one fits better in your test case, but if you are starting with testing, using the toEqual the method will probably be the best alternative.

Comparing Strings

Regular Expression

Jest does have support for comparing strings. Besides, the regular toEqual regex can also be used for comparison. All you need is to call the toMatch method and pass in the regex string.

const text = 'hello world';
test('string comparison', () => {
  expect(text).toMatch(/hello/);
});

Show code in action

Length

It’s also possible to compare the length between two strings using toHaveLength.

expect('abc').toHaveLength(3);

Show code in action

It works with an array as well.

expect([1, 2, 3]).toHaveLength(3);

Show code in action

Comparing Numbers

Besides the basic comparison methods, you can easily compare numbers in your tests by utilizing the following methods:

  • toBeGreaterThanOrEqual
  • toBeGreaterThan
  • toBeLessThanOrEqual
  • toBeLessThan

In the following example, you can use a loop to check if the result is less than 10.

test('loop less than', () => {
  for (let i = 1; i < 10; i++) {
    expect(i).toBeLessThan(10);
  }
});

Show code in action

When changing the value from 10 to 5, you will receive the following error message.

Comparing Arrays

The toContain method is used for array comparison, which checks if the values are included in the list.

test('check an array', () => {
  const weapons = ['phaser', 'laser', 'plasma cannon', 'photon torpedo'];
  expect(weapons).toContain('laser');
});

Show code in action

Comparing Dynamic Values

In a situation where you don’t have an exact value, but you know the type of the object, so you can use the expect.any method.

Primitive Values

For primitive values like string, number, and booleans, you can use:

  • expect.any(String)
  • expect.any(Number)
  • expect.any(Boolean)
test('check dynamic string', () => {
  expect('disruptor').toEqual(expect.any(String));
  expect(1).toEqual(expect.any(Number));
  expect(false).toEqual(expect.any(Boolean));
});

Show code in action

Objects

You can check an Object with objectContaining to see if an object contains some properties inside. In this case, you don’t need to match the same properties from the object you want to evaluate.

test('check dynamic object', () => {
  const weapon = { type: 'laser', damage: 100, range: 10, available: false };
  expect(weapon).toEqual(
    expect.objectContaining({
      damage: expect.any(Number),
      type: expect.any(String),
      available: expect.any(Boolean),
    })
  );
});

Show code in action

Arrays

It’s also possible to use arrayContaining to check the values, and you can even combine them with all previous checks.

test('check dynamic array', () => {
  const weapons = [
    { type: 'phaser', damage: 150, range: 15, speed: 'fast' },
    { type: 'photon cannon', damage: 10000, range: 100, speed: 'slow' },
  ];
  expect(weapons).toEqual(
    expect.arrayContaining([
      expect.objectContaining({
        type: expect.any(String),
        damage: expect.any(Number),
        range: expect.any(Number),
        speed: expect.any(String),
      }),
    ])
  );
});

Show code in action

Asynchronous Code

There are multiple ways to handle asynchronous code, depending on your needs.

Callback

The easiest way to handle callback is to use a single done argument when calling the callback function. For example,

test('test callback', done => {
  initBattleMode((data) => {
    try {
      expect(data).toEqual({ ready: true });
      done();
    } catch (error) {
      done(error);
    }
  });
});

Show code in action

Promise

Asynchronous code with a promise is a lot easier, as all you need to do is to return the promise. Have a look at the modified example:

test('test promise', () => {
  return initBattleMode().then((data) => {
    expect(data).toEqual({ ready: true });
  });
});

Show code in action

async/await

On the other hand, using async/await is a lot more straightforward. So let’s reuse the previous example and modify it to use async/await instead.

test('test async', async () => {
  const data = await initBattleMode();
  expect(data).toEqual({ ready: true });
});

Show code in action

This is just a taste. For a complete list of matches, take a look at the reference docs.

Mocking Fundamentals

Occasionally, when doing our tests, you can’t rely on real data because it’s slow, private, or for other reasons.

Mocking allows you to intercept or erase the actual implementation of a function, capture calls (and the parameters passed in those calls), and enable test-time configuration of returned values.

One way to deal with this situation is to mock (faking) your data. Jest has already built-in some great tools with data mocking. It uses a custom resolver for imports in your tests, making it simple to mock any object outside your test’s scope. In addition, you can use mocked imports with the rich Mock Functions API to spy on function calls with readable test syntax.

Let’s focus on two types of mocks using Jest, the mock function, and the mock module.

Mock Functions

To mock a function, you just need to declare the method as a jest function: jest.fn(), with that, you can start our evaluation. Here is a quick example:

// ...
describe('Rocket Engine', () => {
  const cb = jest.fn();
  beforeEach(() => {
    cb.mockReset();
  });
  test('check callback response', () => {
    cb.mockImplementationOnce(() => 2).mockImplementation(() => 1);
    expect([1, 2, 3].map(cb)).toEqual([2, 1, 1]);
    expect(cb).toHaveBeenCalledTimes(3);
  });
  // ...
});

Show code in action

First, declare your mock method, and then you defined the first and the default outputs. Next, check if the output matches our expected result. Thereafter, check if the method was called the amount of time expected and if the parameters were correct.

If you want to learn more, refer to the reference docs

Mock Modules

Mocking a module works similarly to mocking a function, but instead of applying it to a function, you have to intercept a module import.

Back to the spaceship idea, create a startEngine method that receives a callback function as a parameter and does an HTTP call to an API server. In this case, you have to mock the unfetch module.

import fetch from 'unfetch';

export const startEngine = async (callback) => {
  const res = await fetch('https://api.space.com/rocket/engine/start');
  const json = await res?.json();
  if (json) {
    if (callback) {
      callback(json);
    }
    return json;
  }
  return undefined;
};

Show code in action

Now, declare the values you want to the mock module.

// ...
jest.mock('unfetch', () => () => ({
  json: () =>
    Promise.resolve({
      status: 'ready',
      fuel: '100%',
      power: 100,
      sensors: [{ type: 'temp', value: 50, active: true }],
    }),
}));
// ...

Show code in action

There is also an alternative way to declare your module as an esModule.

// ...
jest.mock('unfetch', () => ({
  __esModule: true,
  default: () => ({
    json: () =>
      Promise.resolve({
        status: 'ready',
        fuel: '100%',
        power: 100,
        sensors: [{ type: 'temp', value: 50, active: true }],
      }),
  }),
}));
// ...

Show code in action

The first parameter is the modules name, and the second one is the factory method. You now have configured it to make the output values always be the same.

describe('Rocket Engine', () => {
  const cb = jest.fn();

  beforeEach(() => {
    cb.mockReset();
  });

  test('check engine response', async () => {
    const data = await startEngine();

    expect(data).toMatchObject({
      power: 100,
      fuel: '100%',
      status: 'ready',
      sensors: [{ type: 'temp', value: 50, active: true }],
    });
  });
});

Show code in action

Using the previous mock, you can check the results. When executing, the output should be the same as declared before.

$ npx jest mock.test.js

For a complete list of mock functions, see the reference docs.

Static Code Analysis in JavaScript

There are a ton of ways your program can break. JavaScript is a loosely typed language. The most common bugs are typos and incorrect types, like the wrong variable name or the sum operation of two strings instead of integers.

What is “Static Analysis”?

So, what does mean “static analysis” of code? The answer is:

Predicting defects in code without running it.

Since JavaScript is a scripting language, instead of the compiler running the code analysis, you need to use formatters and linters to get the job done.

Formatters

Formatters are tools that can fix any style inconsistencies it finds automatically. For this purpose, tools like Prettier or StandardJS can do the job. There are a couple of options to configure it to best match your criteria, and it can be integrated with the most popular editors and IDEs.

To show you how does it work, here is an example of an unformatted code:

function HelloWorld({greeting = "hello", greeted = '"World"', silent = false, onMouseOver,}) {

  if(!greeting){return null};

  // TODO: Don't use random in render
  let num = Math.floor (Math.random() * 1E+7).toString().replace(/\.\d+/ig, "")

  return <div className='HelloWorld' title={`You are visitor number ${ num }`} onMouseOver={onMouseOver}>

    <strong>{ greeting.slice( 0, 1 ).toUpperCase() + greeting.slice(1).toLowerCase() }</strong>
    {greeting.endsWith(",") ? " " : <span style={{color: '\grey'}}>", "</span> }
    <em>
      { greeted }
    </em>
    { (silent)
      ? "."
      : "!"}

  </div>;

}

After using prettier, here is the result:

$ npx prettier --write unformatted_code.jsx
function HelloWorld({ greeting = 'hello', greeted = '"World"', silent = false, onMouseOver }) {
  if (!greeting) {
    return null;
  }

  // TODO: Don't use random in render
  let num = Math.floor(Math.random() * 1e7)
    .toString()
    .replace(/\.\d+/gi, '');

  return (
    <div className="HelloWorld" title={`You are visitor number ${num}`} onMouseOver={onMouseOver}>
      <strong>{greeting.slice(0, 1).toUpperCase() + greeting.slice(1).toLowerCase()}</strong>
      {greeting.endsWith(',') ? ' ' : <span style={{ color: 'grey' }}>", "</span>}
      <em>{greeted}</em>
      {silent ? '.' : '!'}
    </div>
  );
}

As you can see, the main benefit is that you don’t need to worry about these minor inconsistencies anymore. It does that for you automatically.

Remember that you write code for the machine to interpret, but for humans to read.

The clearer and more consistent your code is, the easier it is to understand what is happening.

Linters

Code linting is a way to increase code quality. It analyzes the code and reports a list of potential code quality concerns. Currently, the most used tool for that is ESLint.

ESLint is a tool for identifying and reporting on patterns found in ECMAScript/JavaScript code

Let’s check our example:

function sayHello(name) {
  alert('Hello ' + name);
}

name = 'John Doe';
sayHello(name)

Show code in action

To use ESLint, you need to install it first. Then, open the terminal and type on your project folder.

$ npx eslint --init

Now, you can run ESLint on any file or directory, like in this example.

$ npx eslint unconsistent_code.js

The linter shows where are the errors in our code, based on a set of rules in the eslinrc.{js,json,yaml} file. You can also add, remove, or change any rules. For example, let’s add a rule to check if we are missing a semicolon.

...
rules: {
  semi: ['error', 'always']
},
...

When executed again, the result will show you an error with the new rule.

This was a simple example, but the bigger the project, the more it makes sense to use it and catch many trivial errors that could take some time if done manually.

There are some sets of rules that can be extended, so you won’t need to set them one-by-one like the recommended rules (" extends": "eslint:recommended"), and others made by the community like the Airbnb or Standard that you can include into your project.

For a complete list of rules, refer to the reference docs.

Conclusion

In this article, you’ve learned more about how to start adding tests to your program and understanding the foundations of testing in JavaScript, Jest, Mocking, and Static Code Analysis and that’s only the beginning. But don’t worry about that, the most important thing is to add your tests while you are coding, saving you from numerous problems in the future.

Become part of our own moon mission

View job openings

Running multiple versions of a Stencil design system without conflicts

Microfrontends and reusable Web Components are state-of-the-art concepts in Web Development. Combining both in complex, real-world scenarios can lead to nasty conflicts. This article explores how to run components in multiple versions without conflicts.

Microfrontend Environments (MFE)

In an MFE different product teams work on separate features of a larger application. One team might be working on the search feature, while another team works on the product detail page. Ultimately, all features will be integrated together in the final application.

These features range from being very independent to being closely coupled to other features on the page. Generally speaking, teams try to work as independently as possible, meaning also that they can choose which package dependencies or even frameworks they use – and which versions thereof.

Custom Elements

Web Components are a popular way of sharing and reusing components across applications and JavaScript frameworks today. Custom Elements lie at the heart of Web Components. They can be registered like this:

customElements.define('my-component', MyComponent);

You’re now ready to use <my-component> in the DOM. There can only be one Custom Element for a given tagName.

The Problem

Let’s imagine the following situation: The MFE features should reuse certain components, more specifically they should reuse the Web Components provided by the Design System (DS). The DS is being actively developed and exists in different versions.

As each feature is independent, different teams might use different versions of the Design System. Separate features are developed in isolation and work fine with their specific version of the DS. Once multiple features are integrated in a larger application we’ll have multiple versions of the DS running. And this causes naming conflicts because each Custom Element can only be registered once:

Feature-A uses <my-component> in version 1.2.3 and Feature-B uses <my-component> in version 2.0.0 

Oops! Now what? How do we address this problem? Is there a technical solution? Or maybe a strategic solution?

Forcing feature teams to use the same DS version

One way to address this issue is to let the “shell application” provide one version of the DS. All integrated features would no longer bring their own DS version, but make use of the provided one. We no longer have multiple DS versions running.

While this might work in smaller environments, it’s unrealistic for many complex environments. All DS upgrades would now need to be coordinated and take place at exactly the same time. In our case dictating the version is not an option.

The Design System

The problem is common when reusing Custom Elements in a complex MFE. It’s not specifically created by Custom Elements but it’s one that can be addressed by making small adjustments in the right places of the Custom Elements.

Our hypothetical Design System called “Things” has been built with Stencil – a fantastic tool for building component libraries. All components are using Shadow DOM. Some components are quite independent like <th-icon>. Others are somewhat interconnected like <th-tabs> and <th-tab>. Let’s check out the tabs component and its usage: 

<th-tabs>
  <th-tab active>First</th-tab>
  <th-tab>Second</th-tab>
  <th-tab>Third</th-tab>
</th-tabs>

You can find the full code of the components in their initial state here

A Stencil solution

The first thing we’ll do is enable the transformTagName flag in our stencil.config.ts:

export const config: Config = {
  // ...
  extras: {
    tagNameTransform: true,
  },
  // ...
};

This allows us to register Custom Elements with a custom prefix or suffix.

import { defineCustomElements } from 'things/loader';

// registers custom elements with tagName suffix
defineCustomElements(window, {
  transformTagName: (tagName) => `${tagName}-v1`
});

Great! Feature teams can now register their own custom instances of the components. This prevents naming conflicts with other components and each feature time can work a lot more independently. Alternatively, the “shell application” could provide version-specific instances of the DS.

<!-- using v1 version of the tabs component -->
<th-tabs-v1>...</th-tabs-v1>

<!-- using v2 version of the tabs component -->
<th-tabs-v2>...</th-tabs-v2>

Let’s imagine having 2 versions available. Feature teams can now pick from the provided options without having to provide their own custom versions.

We’re not done, yet

Looking at <th-tabs-v1> we can see that the icon component is no longer rendered. And the click handler even throws an error! So what’s going on here?


Wherever a component references other components we’ll potentially run into problems because the referenced components might not exist.

  • <th-tab-v1> tries to render <th-icon> internally, but <th-icon> does not exist.
  • <th-tab-v1> tries to apply styles to the th-icon selector which no longer selects anything
  • on click, <th-tab-v1> calls a function of <th-tabs>, but <th-tabs> does not exist
  • <th-tabs-v1> provides a method setActiveTab which no longer finds any <th-tab> child element

For every reference to another custom tagName we need to consider that the tagName might have been transformed using transformTagName. As transformTagName executes at runtime our component also needs to figure out the correctly transformed tagNames during runtime. It would be great if Stencil provided a transformTagName function that we could execute at runtime. Unfortunately, that’s not the case. Instead, we can implement a (slightly ugly) solution ourselves.

transformTagName at runtime

export const transformTagName = (tagNameToBeTransformed: string, knownUntransformedTagName: string, knownUntransformedTagNameElementReference: HTMLElement): string => {
  const actualCurrentTag = knownUntransformedTagNameElementReference.tagName.toLowerCase();
  const [prefix, suffix] = actualCurrentTag.split(knownUntransformedTagName);
  return prefix + tagNameToBeTransformed + suffix;
};

This function is not pretty. It requires 3 parameters to return a transformed tagName:

  • tagNameToBeTransformed: tagName that we want to transform, i.e. th-tabs
  • knownUntransformedTagName: untransformed tagName of another component, i.e. th-tab
  • knownUntransformedTagNameElementReference: reference to element with that untransformed tagName, i.e this.el

Usage example:

transformTagName('th-tabs', 'th-tab', this.el); // 'th-tabs-v1'

Note that this.el is a reference to the host element of the Custom Element created by the Element Decorator

Fixing our components

Using our transformTagName function we’re now able to figure out which tagName transformation needs to be considered during runtime.

TypeScript call expressions 

A Custom Element tagName may be referenced in querySelector(tagName)closest(tagName)createElement(tagName) or other functions. Before we call these, we need to find out the transformed tagName.

// Before
this.tabsEl = this.el.closest('th-tabs');

// After
const ThTabs = transformTagName('th-tabs', 'th-tab', this.el);
this.tabsEl = this.el.closest(ThTabs);

JSX element rendering

// Before
public render() {
  return <th-icon />
}

// After
public render() {
  const ThIcon = transformTagName('th-icon', 'th-tab', this.el);
  return <ThIcon class="icon" />;
}

Please note the .icon class, which will be required for the next step.

CSS Selectors

// before
th-icon { /* styles */ }

// after
.icon { /* styles */ }

Wrapping it up

And we’re done!

With a few small changes, we’ve adjusted the codebase to support running multiple versions of the same Custom Elements. This is a huge step for complex Microfrontend Environments. It gives feature teams more freedom in choosing the versions they want to use and releasing them when they want to release. It avoids couplings of features or feature teams. It also reduces coordination and communication efforts.

Find the code of the referenced example project in this Github repo. The second commit shows all required adjustments to support tagName transformations.

Performance considerations

Loading and running multiple versions of the same components at the same time will come with a performance cost. The amount of simultaneously running versions should be managed and minimal.