Categories
Testing

JavaScript Unit Test Best Practices — Edge Cases

Spread the love

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.

By John Au-Yeung

Web developer specializing in React, Vue, and front end development.

Leave a Reply

Your email address will not be published. Required fields are marked *