Share On: Twitter Facebook Google+ LinkedIn Pinterest Tumblr Reddit

Testing JavaScript Applications

Tags: Javascript testing

Testing JavaScript Applications teaches you how to implement an automated testing plan for JavaScript-based web applications. It describes practical testing strategies.

Automated testing will help you write high-quality software in less time, with more confidence, fewer bugs, and without constant manual oversight. Testing JavaScript Applications is a guide to building a comprehensive and reliable JS application testing suite, covering both how to write tests and how JS testing tools work under the hood. You’ll learn from Lucas de Costa, a core contributor to popular JS testing libraries, as he shares a quality mindset for making testing decisions that deliver a real contribution to your business. You’ll benefit from informative explanations and diagrams, easily-transferable code samples, and useful tips on using the latest and most consolidated libraries and frameworks of the JavaScript ecosystem.

Think about what you had to do to see ItemList working, for example. To see ItemList, you had to write the component itself, use it within App, add a few items to the database, and make your application fetch the inventory items from your server. Additionally, you also had to build your front-end application, build a back-end, and migrate and seed databases.

With a story, you can write pages which include various instances of ItemList, with different sets of static data. The first advantage of this technique is that you will be able to see and interact with ItemList much earlier in the development process, even before you start using it in your actual application.

Besides accelerating the feedback loop, stories facilitate collaboration among teams, because they allow anyone, at any moment, to experiment with components and see how they look.

Additionally, by writing stories, you enable others to perform acceptance testing at a component level. Instead of having to create a separate environment for QA or product teams to validate whether your UI is acceptable, with stories, these teams can test each component individually, much more quickly.

In this article, you’ll learn how to write component stories and document your components using Storybook. You’ll start with ItemList, and then proceed to write stories for all components other than App. Once you have multiple stories, I’ll dig deeper into the role that they play in streamlining your development process, fostering collaboration, and improving quality.

After writing stories, I’ll teach you how to document your components and why it’s helpful to do so.

Writing stories

To write your component stories, you’ll use a tool called Storybook. Storybook is capable of loading your stories and displaying them through an organized and understandable UI.

First, install Storybook for React as a dev-dependency with npm install --save-dev @storybook/react. Then, for Storybook to be able to bundle the application which you’ll use to navigate stories, you must install babel-loader using npm install --save-dev babel-loader.

Once you’ve installed these two packages, you’ll have to configure Storybook by creating a .storybook folder in your project’s root directory. Within that folder, you’ll place a main.js configuration file that determines which files Storybook will load as stories.

Listing 1. main.js


module.exports = {
stories: ["../**/*.stories.jsx"], 1
};

1 informing Storybook which are the files containing your stories.

After creating this file, you can already start Storybook by running ./node_modules/.bin/start-storybook.

TIP: To avoid having to type the full path to the Storybook executable whenever you want to run it, add to your package.json file a script called storybook.

Listing 2. package.json


{
  "name":"my-application",
  //..
  "scripts":{
    "storybook":"start-storybook", 1
    //..
  }
  //..
}

1 Creating an NPM script which will start storybook when you execute npm run storybook.

Now you can run Storybook with npm run storybook instead of typing the full path to the start-storybook executable.

When you start Storybook, it will create a web application which allows you to navigate through your component stories. Once it bundles this web application, Storybook will serve it, and open it in a new browser tab.

TIP: To facilitate the exchange of information between your development, design, and product, teams you can deploy the application generated by Storybook to a place where every member of these teams will have access.

To create your first story for the ItemList component, add a file called ItemList.stories.jsx. Within this file, you’ll export an object with the metadata for the group of stories you’ll write and the name of each story you want Storybook to display.

To write an individual story, create a named export whose value is a function which returns the component you want to showcase.

Listing 3. ItemList.stories.jsx


import React from "react"
import { ItemList } from "./ItemList";
export default { 1
title: "ItemList",
component: ItemList,
includeStories: ["staticItemList"]
};
export const staticItemList = () => 

1 Configuring the set of stories for ItemList, informing the title for those stories, which component they relate to and which stories to include.

2 Creating a story which renders an instance of ItemList with a static list of items.

Once you’ve written this story, check your Storybook instance and you’ll see that it renders an ItemList, just like App does. Because you’ve written static data to populate ItemList, you don’t need any servers running, nor any data that comes from the rest of your application.

As soon as your component can be rendered, you can see and interact with it through a story.

Now that you have a story for ItemList, everyone in your team will be able to see how it looks like, and interact with it atomically. Whenever they need to change ItemList, they can quickly iterate by using your story, instead of having to deal with your entire application.

Despite this story making it quicker and more accessible for people to change and interact with ItemList, it doesn’t yet demonstrate all the functionality of this component.

To show how ItemList will animate items entering or leaving the inventory, and therefore cover the full range of this component’s capabilities, you’ll write a new story. This story should return a stateful component which includes ItemList and two buttons which update the outer component’s state. One of the buttons will add an item to the list, and the other will reset ItemList to its original state.

Listing 4. ItemList.stories.jsx


import React, { useState } from "react";
// ...
export default {
title: "ItemList",
component: ItemList,
includeStories: ["staticItemList", "animatedItems"]
};
// ...
export const animatedItems = () => { 1
    const initialList = { cheesecake: 2, croissant: 5 }; 2
        const StatefulItemList = () => { 3
        const [itemList, setItemList] = useState(initialList); 4
        const add = () => setItemList({ ...initialList, macaroon: 96 }); 5
        const reset = () => setItemList(initialList); 6
        return ( 7
            <div>
                <ItemList itemList={itemList} />
                <button onClick={add}>Add item
                <button onClick={reset}>Reset
            </div>
        );
    };
        return  8
};

1 A story to demonstrate how ItemList animates items entering or leaving it.

2 Creating a static list of items.

3 The stateful component which the story will render.

4 A piece of state containing an item list.

5 A function which adds ninety-six macaroons to the list.

6 A function which resets the list to its initial state.

7 Causing the stateful component to return buttons to add items, reset the list of items, and a div with an instance of ItemList whose itemList prop is the list of items in the stateful component’s state.

8 Rendering an instance of the stateful component.

Whenever you need to make it possible to interact with your components, you can create a stateful wrapper, as you’ve just done. The problem with these wrappers is that they add an extra layer of complexity to your stories, and constrain the viewer’s interactions to what you’ve initially thought they’d want to do. Instead of using stateful wrappers, you can use a package called @storybook/addon- knobs to allow viewers to manipulate the props passed to your components in whichever way they want.

The @storybook/addon-knobs add-on adds a new tab to Storybook’s bottom panel in which viewers can change, on-the-fly, the values of any props associated with your stories.

Go ahead and install @storybook/addon-knobs as a dev-dependency with npm install -- save-dev @storybook/addon-knobs. Then, update your .storybook/main.js file and add an addons property to it. This property’s value will be an array containing the list of add- ons that Storybook should load.

Listing 5. main.js


module.exports = {
stories: ["../**/*.stories.jsx"],
addons: ["@storybook/addon-knobs/register"] 1
};
1 Configuring Storybook to use the @storybook/addon-knobs addon. With this add-on, you can update your stories so that @storybook/addon-knobs will manage the props passed to your components.

Listing 6. ItemList.stories.jsx


import React from "react";
import { withKnobs, object } from "@storybook/addon-knobs";
export default {
    title: "ItemList",
    component: ItemList,
    includeStories: ["staticItemList", "animatedItems"],
    decorators: [withKnobs] 1
};
// ...
export const animatedItems = () => {
    const knobLabel = "Contents";
    const knobDefaultValue = { cheesecake: 2, croissant: 5 }; 2
    const itemList = object(knobLabel, knobDefaultValue); 3
    return <ItemList itemList={itemList} />
};

1 Configuring the stories for ItemList so that they can use the knobs add-on.

2 Creating an ItemList object which will be managed by the knobs add-on.

3 Rendering an instance of ItemList whose ItemList prop is the object managed by knobs.

Once you’ve used your new add-on to pass a managed property to ItemList, open storybook and try to change the itemList prop through the "Knobs" tab on the bottom of the story called "animated items." As you change these properties, you’ll see that the component updates, animating the items entering or leaving the list. The flexibility provided by @storybook/addon-knobs makes it easier for testers to scrutinize your components, simulate edge-cases, and perform exploratory testing. For product teams, this flexibility will result in a better insight into your component’s capabilities.

Now that you’ve written stories for ItemList, you’ll write one for ItemForm too. Within your project’s root directory, create a file called ItemForm.stories.jsx and write a story which renders your form and shows an alert when users submit it.

Listing 7. ItemForm.stories.jsx


import React from "react";
import { ItemForm } from "./ItemForm";
export default { 1
    title: "ItemForm",
    component: ItemForm,
    includeStories: ["itemForm"]
};
export const itemForm = () => { 2
    return (
        <ItemForm
            onItemAdded={(...data) => {
                alert(JSON.stringify(data));
            }}
        />
    );
};

1 Configuring the set of stories for ItemForm, informing their title, the component to which they relate, and which stories to include.

2 A story for ItemForm which displays an alert when an item is added.

Even though this story renders your component, and shows an alert with the submitted data, ItemForm is still sending requests to your back-end. If you’re running your server while interacting with this component’s this story, you’ll see that your database does get updated when you submit ItemForm. To avoid ItemForm sending any requests to your back-end, you must stub that functionality.

Previously you have used nock to create interceptors which would respond to HTTP requests, but you won’t be able to use it in Storybook. Because nock depends on Node specific modules, like fs, it can’t run on your browser. Instead of using nock to intercept and respond to HTTP requests, you’ll use a package called fetch-mock instead. Its API is similar to nock’s, and it works in a browser. Install fetch-mock as a dev-dependency with npm install --save-dev fetch-mock, and update ItemForm.stories.jsx so that you have an interceptor for the POST request that ItemForm performs.

Listing 8. ItemForm.stories.jsx


// ...
import fetchMock from "fetch-mock";
import { API_ADDR } from "./constants";
// ...
export const itemForm = () => {
    fetchMock.post(`glob:${API_ADDR}/inventory/*`, 200); 1
    return (
        <ItemForm
            onItemAdded={(...data) => {
                alert(JSON.stringify(data));
            }}
        />
    );
};
1 Creating an interceptor which responds to any POST requests to /inventory/* with a 200 status.

After using fetch-mock to intercept requests, ItemForm will never reach your back-end, and you will always get successful responses. To confirm that ItemForm doesn’t dispatch any HTTP requests, try interacting with your form’s story and submitting a few items while having your developer tool’s network tab open.

Now, the last step to make this story complete is to clear the interceptor you’ve written so that it won’t interfere in other stories. Currently, when you open your form’s story, it will create an interceptor which will persist until the user refreshes the story viewer. That interceptor could affect other stories, if, for example, you had another story which sent requests to the same URL as ItemForm does.

To clear your interceptors when users navigate away from this story, you’ll wrap ItemForm into another component, which creates an interceptor when it mounts and eliminates the interceptor when it unmounts.

Listing 9. ItemForm.stories.jsx


// ...
export const itemForm = () => {
const ItemFormStory = () => {
    useEffect(() => { 1
        fetchMock.post(`glob:${API_ADDR}/inventory/*`, 200)
        return () => fetchMock.restore(); 2
    }, []);
    return (
        <ItemForm
            onItemAdded={(...data) => {
             alert(JSON.stringify(data));
            }}
        />
        );
    }
    return  <ItemFormStory />
};
1 When the ItemFormStory mounts, creates an interceptor which responds to any POST requests to /inventory/* with a 200 status. 2 Causes the story to destroy the interceptor when it unmounts.

When using stubs within your stories, remember to clear any hanging stubs or spies that you have created, as you’ve just done when restoring your interceptor. To perform both the stubbing and the cleanup, you can use wrapper components with hooks or lifecycle methods.

At last, you’ll get rid of the alert triggered by ItemForm. Instead of showing a disruptive pop-up, you’ll use the @storybook/addon-actions package to log actions to a separate tab in the Storybook UI.

To use this add-on, install it as a dev-dependency with npm install --save-dev @storybook/addon-actions and update your Storybook configuration file. Within .storybook/main.js, add an addons property to the exported object and assign to it an array containing the add-on’s registration namespace.

Listing 10. main.js


module.exports = {
    stories: ["../**/*.stories.jsx"],
    addons: [
    "@storybook/addon-knobs/register",
    "@storybook/addon-actions/register" 1
    ]
};

1 Configures Storybook to use the @storybook/addon-actions add-on.

After installing this add-on and rerunning Storybook, you’ll see an "actions" tab at the bottom of each story. Within this tab, Storybook will log each call to the actions created by addon-actions.

To start logging actions, you’ll update ItemForm.stories.js. In this file, you’ll import action from @storybook/addon-actions, and use this function to create the callback you’ll pass to the ItemForm instance in your story.

Listing 11. ItemForm.stories.jsx


// ...
import { action } from "@storybook/addon-actions";
// ...
export const itemForm = () => {
    const ItemFormStory = () => {
        // ...

        return <ItemForm
            onItemAdded={action("form-submission")} 1
        />;
    };
    return <ItemFormStory />;
};

1 Causes the Form to log actions to the Actions tab within Storybook when submitted.

Once you’ve updated your form’s story, open Storybook and try submitting the form a few times. Every time you submit it, Storybook should log a new action should to the story’s "actions" tab.

Using actions instead of alerts makes it much easier to understand what your component is doing, and inspect the arguments with which it’s invoking the passed callbacks.

Now that you know how to create stories, try, as an exercise, to create a story for the ActionLog component. Create a new .stories.jsx file, and write a story which demonstrates how ActionLog works.

In addition to shortening the feedback loop, and creating a friendlier environment for others to test components manually, these stories also facilitate the communication among developers and members of other teams. When designers have access to stories, it’s easier for them to prepare layouts which adhere to existing UI patterns because they know how the application’s current components look and how they behave.

Ultimately, stories are a step towards the industrialization of UI. By trying to constrain the development of new functionality to an existing set of components, you reduce rework, and end-up with more reliable applications. This improvement happens not only because you have more time to focus on tests, but also because with less code, there are fewer places for bugs to hide.

After reading the paragraph above, many will probably argue that the industrialization of UI will limit creativity — a point with which I entirely agree. Nonetheless, I’d say that this limitation is a feature, not a bug. Creativity comes at a cost, which often goes unnoticed because of how often product teams reach for new components. Trying to limit UI patterns to a set of existing components makes the amount of work needed to implement new functionality more noticeable for other teams.

The goal with component libraries is not to limit a designer’s freedom to create, rather to make the cost of creativity explicit, so that the business can thrive. Libraries of this kind, representing a set of UI components, constraints, and best practices, are also called a design system and have experienced a spike in popularity in recent years.

Despite this rise in popularity, stories aren’t always a good idea. Just like tests, stories are pieces of code which need maintenance. As you update your components, you need to make sure that your stories still adequately demonstrate your component use- cases.

Even if you have the bandwidth to keep stories up-to-date, you’ll still pay a higher price for maintenance. The advantage of bearing these costs is that you will reduce the costs involved in conceptualizing and implementing new functionalities. This cost reduction happens because stories facilitate the reuse of components and limit the design team’s creativity to what already exists, making the cost of change more explicit.

Writing documentation

Louis is confident in his ability to make customer’s eyes gleam on every dessert they taste. Still, he knows that for his staff to do the same, his recipes must be carefully explained, from the most discreet cocoa nibs to the boldest portions of whipped cream. By writing stories, you can show what a component looks like, and demonstrate how it behaves, but for others to understand how they’re supposed to use it, you’ll have to write documentation.

In this section, you’ll learn how to use Storybook to write and publish documentation for your components, starting with ItemList. To write documentation, you’ll use a file format called MDX. MDX files support a combination of markdown and JSX code so that you can write plain-text explaining how your component works, and include real instances of your components throughout the documentation itself.

For Storybook to support MDX files, you’ll use the @storybook/addon-docs add-on. This add-on will cause each of your stories to display an extra tab called docs. In this tab, you’ll find the MDX documentation corresponding which corresponds to the current story.

When installing @storybook/addon-docs, you must the react-is package upon which this add-on depends. To install both as dev dependencies, execute npm install --save-dev react-is @storybook/addon-docs. Once @storybook/addon-docs and its dependency have been installed, update the configurations within .storybook/main.js so that Storybook supports documentation written in MDX.

In addition to updating the addons property in your configurations, you’ll also have to update the stories property so that Storybook will include files with an .mdx extension.

Listing 12. main.js


module.exports = {
        stories: ["../**/*.stories.@(jsx|mdx)"],
        addons: [
            "@storybook/addon-knobs/register",
            "@storybook/addon-actions/register",
            {
                name: "@storybook/addon-docs", 1
                options: { configureJSX: true } 2
            }
        ]
};

1 Configures Storybook to use the @storybook/addon-docs add-on.

2 Given your current Babel configuration, this option is necessary to make sure that the add-on will be able to handle JSX files.

After updating this file, rerun Storybook and access one of your stories to see the "docs" tab at the top.

Now that you’ve configured this add-on, you’ll write the contents for the item list’s docs tab.

Go ahead and create a file called ItemList.docs.mdx, in which you will use markdown to describe how your component works, and JSX to include real ItemList instances to illustrate your documentation.

For Storybook to adequately render your component’s instance, don’t forget to wrap it into the Preview and Story components exported by @storybook/addon-docs. Additionally, to link the necessary metadata to your story, you’ll also have to import the add- on’s Meta component and add it to the beginning of your file.

NOTE: You can find the MDX format’s complete documentation at mdxjs.com.

Listing 13. ItemList.docs.mdx


import { Meta, Story, Preview } from '@storybook/addon-docs/blocks';
import { ItemList } from './ItemList';
<Meta title="ItemList" component={ItemList} />
# Item list

The `ItemList` component displays a list of inventory items.
It's capable of:
* Animating new items
* Highlighting items which are about to become unavailable
## Props
* An object in which each key represents an item's name, and each value represents
its quantity.
    <Preview>
            <Story name="A list of items">
            <ItemList itemList={{
        cheesecake: 2,
        croissant: 5,
        macaroon: 96
        }} />
        </Story>
    </Preview>

After you’ve written some documentation for ItemList, open its stories on Storybook and check the Docs tab, so that you can see how your MDX files will look like.

Good documentation aids testers in determining a component’s expected behavior.

Especially by writing documentation within Storybook, where your components live, you can clearly communicate to testers what is a component’s expected behavior. In turn, clear communication leads to quicker and more effective testing with less overhead, which can reduce costs.

Additionally, proper documentation also aids product teams in designing new features in such a way that reduces implementation time, freeing engineers to focus on reliability and other important aspects of software development. That’s all for this article.

If you want to learn more about the book, you can check it out on Manning’s browser- based liveBook platform here.

Comments(1)

avatar of commentor3
jack wilson Thank you Sir Node.. good article. April 17th 2021, 1:10 am