Categories
Testing

JavaScript Unit Test Best Practices — Hooks and APIs

Unit tests are very useful for checking how our app is working.

Otherwise, we run into all kinds of issues later on.

In this article, we’ll look at some best practices we should follow when writing JavaScript unit tests.

Properly Set Up the Actions that Apply to All the Tests Involved

If we’re running the same thing before every test, we should put it in a beforeEach hook.

This way, we run the same piece of code before each test without repeating the code.

For example, we can write:

describe('Saving the user profile', () => {

  beforeEach(() => {
    login();
  });

  it('should save updated profile setting to database', () => {
    //...

    expect(request.url).toBe('/profiles/1');
    expect(request.method).toBe('POST');
    expect(request.data()).toEqual({ username: 'james' });
  });

  it('should notify the user', () => {
    //...
  });
});

  it('should redirect user', () => {
    //...
  });
});

Likewise, if we have some code that we’ve to run after each test, we should have an afterEach hook that runs after each test:

describe('Saving the user profile', () => {

  beforeEach(() => {
    login();
  });

  afterEach( () => {
    logOut();
  });

  it('should save updated profile setting to database', () => {
    //...

    expect(request.url).toBe('/profiles/1');
    expect(request.method).toBe('POST');
    expect(request.data()).toEqual({ username: 'james' });
  });

  it('should notify the user', () => {
    //...
  });

  it('should redirect user', () => {
    //...
  });
});

Consider Using Factory Functions in the Tests

Factory functions can help reduce setup code.

They make each test more readable since creation is done in a single function call.

And they provide flexibility when creating new instances.

For instance, we can write:

describe('User profile module', () => {
  let user;

  beforeEach(() => {
    user = createUser('james');
  });

  it('should publish a topic "like" when like is called', () => {
    spyOn(user, 'notify');
    user.like();
    expect(user.notify).toHaveBeenCalledWith('like', { count: 1 });
  });

  it('should retrieve the correct number of likes', () => {
    user.like();
    user.like();
    expect(user.getLikes()).toBe(2);
  });
});

We have the createUser function to create a user with one function call.

This way, we don’t have to write the same setup code for every test.

We can also use them with DOM tests:

function createSearchForm() {
  fixtures.inject(`<div id="container">
    <form class="js-form" action="/search">
      <input type="search">
      <input type="submit" value="Search">
    </form>
  </div>`);

  const container = document.getElementById('container');
  const form = container.getElementsByClassName('js-form')[0];
  const searchInput = form.querySelector('input[type=search]');
  const submitInput = form.querySelector('input[type=submit]');

  return {
    container,
    form,
    searchInput,
    submitInput
  };
}

describe('search component', () => {
  describe('when the search button is clicked', () => {
    it('should do the search', () => {
      const { container, form, searchInput, submitInput } = createSearchForm();
      //...
      expect(search.validate).toHaveBeenCalledWith('foo');
    });

    // ...
  });
});

We have the search form creation code in the createSearchForm function.

In the function, we return various parts of the form’s DOM objects to let us check the code.

Using Testing Framework’s API

We should take advantage of a test framework’s API.

This way, we make use of its functionality to make testing easier.

For example, we can write:

fit('should call baz with the proper arguments', () => {
  const foo = jasmine.createSpyObj('foo', ['bar', 'baz']);
  foo.baz('baz');
  expect(foo.baz).toHaveBeenCalledWith('baz');
});

it('should do something else', () => {
  //...
});

using Jasmine.

We spied on stubbed functions to see if they’re called with the createSpyObj method.

And we use fit so that only the first test is run.

Conclusion

We should make sure of the testing framework’s API to make testing easier.

Also, we should make sure we put repeated code in hooks to avoid repetition.

Categories
Testing

JavaScript Unit Test Best Practices — Edge Cases

Unit tests are very useful for checking how our app is working.

Otherwise, we run into all kinds of issues later on.

In this article, we’ll look at some best practices we should follow when writing JavaScript unit tests.

Use Realistic Input Data

We should use realistic input data in our tests so that we know what we’re testing with.

To make generate fake data easily, we can use the Faker package.

It can generate names, username, company names, credit card numbers, and more.

For example, we can write:

it("should add product", async () => {
  const addProductResult = addProduct(faker.commerce.productName(), faker.random.number());
  expect(addProductResult).to.be.true;
});

We have a test to add a product with realistic names and ID number so that we can understand the result.

Test Many Input Combinations

We should test many input combinations.

This way, we won’t only choose cases that we know will make our test pass.

We can make the values random.

And we can also several permutations of some data in our test.

For example, with the fast-check library, we can create random combinations of data for our test:

import fc from "fast-check";

describe("Product service", () => {
  describe("add new product", () => {
    it("add product with various combinations successfully", () =>
      fc.assert(
        fc.property(fc.integer(), fc.string(), (id, name) => {
          expect(addNewProduct(id, name).status).toEqual("success");
        })
      ));
  });
});

We called addnewProduct with random values of id and name and check if the returned status is 'success' .

This way, we can’t rig our test to pass with only some values.

Use Only Short and Inline snapshots

We should use short and inline snapshots in or tests to let us create UI tests that fast to run.

If they can be added inline, then we know that they’ll be small.

If it’s so big that it can only be stored in an external file, then it’ll probably slow down our tests too much.

For example, we can write:

it("when we go to example.com, a menu is displayed", () => {
  const receivedPage = renderer
    .create(<DisplayPage page="http://www.example.com">Example</DisplayPage>)
    .toJSON();

  const menu = receivedPage.content.menu;
  expect(menu).toMatchInlineSnapshot(`<ul>
    <li>Home</li>
    <li>Profile</li>
    <li>Logout</li>
  </ul>`);
});

We render the DisplayPage component and then check against the snapshot that we created inline.

Avoid Global Test Fixtures and Seeds

We should create our data per test and clean them out after each test.

This way, we always get a clean environment for our tests.

And the tests won’t depend on each other.

This is important since we’ll run into problems when tests depend on each other.

If performance becomes a concern with creating data for each test, then we’ve to simplify the data.

So if we test with database interaction, we got to remove all the data after each test.

Expect Errors

If we expect errors, then we document the errors that are thrown into our app.

In most JavaScript test frameworks, we have something like:

expect(method).toThrow())

to check if method throws something after we do something.

Sweeping errors under the rug just make them hard to find.

And it still doesn’t do what we expect.

So we can write something like:

it("when no data provided, it throws error 400", async () => {
  await expect(addUser({}))
    .to.eventually.throw(AppError)
    .with.property("code", "invalid input");
});

Conclusion

We should use realistic data for tests.

Also, we use inline snapshots to make our tests faster.

We also should test with many kinds of inputs.

Categories
Testing

JavaScript Unit Test Best Practices — Concerns

Unit tests are very useful for checking how our app is working.

Otherwise, we run into all kinds of issues later on.

In this article, we’ll look at some best practices we should follow when writing JavaScript unit tests.

Don’t Test Multiple Concerns in the Same Test

We should only test one thing in each test to keep things simple.

It’ll help us locate any problems easier if we find issues.

For example, instead of writing:

it('should save profile data and update the view with profile data', () => {
  // expect(...)to(...);
  // expect(...)to(...);
});

We write:

it('should save profile data', () => {
  // expect(...)to(...);
});

it('should update the view with profile data', () => {
  // expect(...)to(...);
});

Writing ‘and’ or ‘or’ in the test description means that we should break them up into multiple tests.

Cover the General Case and the Edge Cases

We got to cover edge cases so that we won’t miss anything.

They’re easy to miss but people will notice them.

For instance, instead of writing:”

it('should properly calculate arithmetic expression', () => {
  const result = calculate('(5 + 10) / 2');
  expect(result).toBe(7.5);
});

We write:

describe('The arithmetic expression calculator', () => {
  it('should return null when the expression is an empty string', () => {
    const result = calculator('');
    expect(result).toBeNull();
  });

  it('should return value if expression is the value', () => {
    const result = calculator('12');
    expect(result).toBe(12);
  });

  it('should properly calculate arithmetic expression', () => {
    const result = calculate('(5 + 10) / 2');
    expect(result).toBe(7.5);
  });

  it('should throw an error whenever an invalid expression is passed', () => {
    const calc = () => calculator('1 + 1 -');
    expect(calc).toThrow();
  });
});

Always Start by Writing the Simplest Failing Test with TDD

We should always write a failing test first with TDD.

Then we can build our functionality to make it pass.

Always Make Small Steps in Each Test-First Cycle

Making small steps in each test first cycle would let us work our way from failing to passing tests.

For instance, instead of writing:

it('should properly calculate arithmetic expression', () => {
  const result = calculate('((5 + 10) / 2)  + 10');
  expect(result).toBe(17.5);
});

to test everything, we test small cases separately ane make each case pass:

describe('arithmetic expression calculator', () => {
  it('should return null when the expression is an empty string', () => {
    const result = calculator('');
    expect(result).toBeNull();
  });

  it('should return value if the expression only has one value', () => {
    const result = calculator('12');
    expect(result).toBe(12);
  });

  describe('addition', () => {
    it('should properly calculate a simple addition', () => {
      const result = calculator('10 + 1');
      expect(result).toBe(11);
    });

    it('should properly calculate a complex addition', () => {
      const result = calculator('1 + 2 + 3');
      expect(result).toBe(6);
    });
  });

  // ...

  describe('complex expressions', () => {
    it('should properly calculate an expression containing all operators', () => {
      const result = RPN('5 + (10 / 2) * 2 - 1'));
      expect(result).toBe(14);
    });
  });
});

We go from the simplest cases to the most complex cases with our tests.

This way, we know we won’t miss much if anything.

Conclusion

We should test one thing in each test.

Also, we should have a test to test anything from the simplest to the most complex cases.

Categories
Testing

JavaScript Unit Test Best Practices — CI and Quality

Unit tests are very useful for checking how our app is working.

Otherwise, we run into all kinds of issues later on.

In this article, we’ll look at some best practices we should follow when writing JavaScript unit tests.

Tests as a Live Collaborative Document

Tests tell a lot about what our system does.

They have lots of descriptions and checks that show us what it can do.

There’re also things like Storybooks and UI components that tell us what they do.

We can look through each component with them.

Detect Visual Issues with Automated Tools

We can use various tools to detect visual issues.

They aren’t captured in unit or integration tests.

These tools save screenshots of our app so that we can see any flaws in our app.

There’re also tools like Applitools, Percy.io that let us manage the screenshots and detect visual noise with their own technology automatically.

Get Enough Coverage for Being Confident

We can get enough test coverage to make us confident in our tests.

Around 80% should cover the most important parts.

We can use CI tools to check if we meet the threshold for test coverage.

Most testing frameworks can get test coverage without much trouble.

Inspect Coverage Reports

We should check our test coverage reports to check what parts of our app hasn’t been covered by tests yet.

They can highlight the items in our code to check.

Just having the test coverage number doesn’t tell us which parts of our app are covered with tests.

Measure Logical Coverage

We can use mutation testing to tell which part of our app is actually tested rather than just visited.

To do that, we can intentionally change the values to check the outcomes as a result of them.

We may uncover cases that should fail that doesn’t come up that we may have not covered yet.

Preventing Test Code Issues

To check for issues with test code, we can use test linters to check for code.

There’re are plugins for ESLint like eslint-plugin-mocha or eslint-plugin-jest that’ll do these checks for us.

It’ll warn us when tests have no assertions for example.

Enrich Linters and Cancel Builds that have Linting Issues

If we have linting issues with our code, then we should stop them from being built since we want the linting issues to be fixed.

There’re many plugins for ESLint like the Airbnb and Standard.js plugins.

They can discover code issues and make sure the formatting is what everyone can agree on.

There’s also the eslint-plugin-security to check for security issues in our apps.

This will stop attackers from trying to attack our app.

Shorten the Feedback Loop

We can get feedback faster is we run CI locally on the developer’s machine.

They let us run our pipeline locally so that we can get useful testing insights.

So we can write our code, get feedback, and then make changes as needed.

Conclusion

We can enhance our testing workflows with CI tools and checking for things like visual defects.

Categories
Testing

JavaScript Unit Test Best Practices — Basics

Unit tests are very useful for checking how our app is working.

Otherwise, we run into all kinds of issues later on.

In this article, we’ll look at some best practices we should follow when writing JavaScript unit tests.

Unit Tests

Each unit test tests a unit of work.

It can involve multiple methods or classes that return a value or throw an exception.

They can also test state changes in the system.

We can also test 3rd party API calls.

Each test should be isolated from each other.

Any behavior should have only one test.

And they can’t affect others.

Unit Tests are Lightweight Tests

Lightweight tests are repeatable, fast, consistent, and easy to write and read.

Unit tests also code.

They should meet the same quality as the code being tested.

They can be refactored to make them more maintainable and readable.

Design Principles

Our code should be written to be testable.

We should avoid good naming conventions and comment about why we have the code.

Comments aren’t a replacement for bad design.

Also, we should avoid any repetition.

Each piece of code should have a single responsibility so that a unit test can test it.

Our code should also have a single level of abstraction in the same component.

We shouldn’t mix business logic with other technical details in the same method.

Also, our code should be configurable so that we can run our code in any environment when testing.

Also, we should adopt patterns like dependency injection to separate object creation from business logic.

Global mutable state are also hard to test and debug, so we should avoid them.

Use TDD

Test-driven development is a good way to test software components interactively.

This way, we can check the behavior of each part with each test.

The workflow is that we start with writing a failing test.

Then we make the test pass with new code changes.

Finally, we clean up the code and make sure it still works with our test.

The test first cycle makes the code design testable.

Small changes are maintainable.

And the codebase can easily be enhanced with refactoring since we can test it with tests.

It’s also cheaper to change the ode frequently and in small increments.

Tests generate confidence to add features, fix bugs, and explore new designs.

Structure Tests Properly

We should structure our tests properly.

We can nest subsets of tests.

Instead of writing:

describe('A set of functionalities', () => {
  it('should do something nice', () => {
  });

  it('some feature should do something great', () => {
  });

  it('some feature should do something good', () => {
  });

  it('another subset of features should also do something ', () => {
  });
});

We can write:

describe('A set of functionalities', () => {
  it('should do something nice', () => {
  });

  describe('some feature', () => {
    it('should do something great', () => {
    });

    it('should do something awesome', () => {
    });
  });

  describe('another subset of features', () => {
    it('should also do something great', () => {
    });
  });
});

However, don’t go overboard with nesting since it’s hard to read.

Conclusion

Unit tests are tests that test a small part of an app. We can organize them into groups.