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.

Categories
Testing

JavaScript Problems — Click Events, Unit Tests, Page Load, and More

Like any kind of apps, there are difficult issues to solve when we write JavaScript apps.

In this article, we’ll look at some solutions to common JavaScript problems.

Tools for Unit Testing JavaScript Code

There are many test runners for creating and running unit tests with JavaScript apps.

Ava is a fast test runner that lets us run unit tests. It supports promises and ES6 syntax. Also, the environment is isolated for each file.

It also supports generators and async functions. Observables are also supported. Stack traces are easier to read than other runners as it’s clean.

Buster.js is another test runner. It’s cross-platform and it runs on Windows, Mac, and Linux. Tests run from a browser headlessly. Multiple clients can run the tests at the same time. Node apps can also be tested. xUnit or BDD style tests can be written.

Multiple JavaScript test frameworks are supported. It comes with SinonJS. Tests are run automatically on save.

Jasmine is a BDD test framework. The syntax of it looks like the RSpec syntax, which is used to test Ruby and Rails apps.

QUnit lets us test JavaScript front end code. It’s mainly based on jQuery, jQuery UI, and jQuery Mobile.

Sinon is another testing tool. It’s a simple test runner that lets us mock code and run them.

It has no dependencies.

Jest is another powerful test framework with a built-in test runner and many tools for mocking things to help us tets. It supports all modern syntax, It supports async and modules, including mocking them.

JavaScript Static Variables

We can add static variables by directly attaching a property directly to the constructor.

For instance, if we have:

function MyClass () {  
  //...  
}

Then we can write:

MyClass.staticProp  = "baz";

We attach staticProp to MyClass directly.

Check that a Number is a Float or an Integer

We can check if a number is an integer by using the remainder operator to get the remainder of it when we divide it by 1.

For example, we can write;

const isInt = (n) => {  
  return n % 1 === 0;  
}

If it’s an integer, then it’s true .

We can check if it’s a floating-point number by changing === to !== . We write:

const isFloat = (n) => {  
  return n % 1 !== 0;  
}

Check Which Key is Pressed on the Keyboard

We can use the keypress event’s keyCode or which property.

They both have the integer code for the keyboard key that is pressed.

For example, we can write:

const code = e.keyCode || e.which;  
if(code === 13) {   
  //Do something  
}

We check if it’s 13 to check for the Enter key.

window.onload vs document.onload

There is a difference between window.onload and document.onload .

window.onload is fired when the entire page loads, including all content like images, CSS, scripts, and other things.

documebt.onload is called when the DOM is ready which can be prior to images and other external content is loaded.

Self-References in Object Literals

We can use this to reference the object that this is currently in.

For instance, if we have:

const foo = {  
  a: 1,  
  b: 2,  
  get c() {  
    return this.a + this.b;  
  }  
}

Then this.a is 1 and this.b is 2, so c is 3.

We can only do this with top-level methods, including getters and setters.

Otherwise, the value of this maybe different.

addEventListener vs onclick

To attach a click listener to an element, we can do it a few ways.

We can call addEventListener with the 'click' event.

Also, we can add the onclick attribute to an element.

We can also get the element and set a callback as the value of the onclick property.

For example, we can write:

element.addEventListener('click', onClick, false);

onClick is a function.

false disables capture mode, which propagates events from the root element down.

Likewise, we can write:

<a href="#" onclick="alert('hello');">click me</a>

We have the a element with the onclick attribute that has JavaScript expressions in it.

Likewise, we can write:

element.onclick = () => {   
  //...  
};

We set a listener function to the onclick method of a DOM element object.

They do the same thing.

Conclusion

We can attach click listeners in various ways,

Also, we can use this to reference itself in top-level methods.

There are also many tools for writing and running unit tests for JavaScript apps.