DEV Community

John Au-Yeung
John Au-Yeung

Posted on • Originally published at thewebdev.info on

JavaScript Unit Test Best Practices — Testing Behavior

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.

Test the Behavior and not the Internal Implementation

We should only testy the result and not worry about the internal implementation.

This way, we won’t worry ourselves with something that doesn’t need to be checked in tests.

For instance, we shouldn’t test internal variables:

it('should add a user to database', () => {
  userManager.addUser('james', 'password');

  expect(userManager._users[0].name).toBe('james');
  expect(userManager._users[0].password).toBe('password');
});

Instead, we write:

it('should add a user to database', () => {
  userManager.addUser('james', 'password');
  expect(userManager.login('james', 'password')).toBe(true);
});

We just test the returned results instead of internal variables.

This way, we don’t have to change our tests when the implementation changes.

Don’t Mock Everything

We shouldn’t mock everything.

This way, at least we’re testing something.

For instance, we can write:

describe('when the user has already visited the page', () => {
  describe('when the message is available', () => {
    it('should display the message', () => {
      const storage = new MemoryStorage();
      storage.setItem('page-visited', '1');
      const messageManager = new MessageManager(storage);
      spyOn(messageManager, 'display');
      messageManager.start();
      expect(messageManager.display).toHaveBeenCalled();
    });
  });
});

We use a memory storage solution instead of real local storage.

This way, our test doesn’t commit any side effects with our test.

We didn’t mock messageManager.display since we just want to check it’s called.

We use real versions of objects if it’s simple to set up.

They also shouldn’t create shared states between tests.

The speed of the real object should be fast if it’s used.

The real object also shouldn’t make any network requests or browser page reloads.

Create New Tests for Every Defect

There should be new tests for all defects that are fixed.

This way, we can fix it and never have it appear again in the same form.

Don’t Write Unit Tests for Complex User Interactions

Unit tests should be used to test simple actions.

If we have more complex workflows we want to test, then we should add integration or end to end tests.

They’re all needed for more complex workflows like filling forms and submitting data, etc.

Functional tests can be written with frameworks like Selenium or Cypress.

Test Simple User Actions

We should test simple user actions like clicks and inputs.

For example, we can write:

describe('clicking on the "Preview profile" link', () => {
  it('should show the profile preview if it is hidden', () => {
    const button = document.querySelector('button');
    const profileModule = createProfileModule({ visible: false });
    spyOn(profileModule, 'show');
    button.click(previewLink);
    expect(profileModule.show).toHaveBeenCalled();
  });

  it('should hide the profile preview if it is displayed', () => {
    const button = document.querySelector('button');
    const profileModule = createProfileModule({ visible: true });
    spyOn(profileModule, 'hide');
    button.click();
    expect(profileModule.hide).toHaveBeenCalled();
  });
});

We have the profileModule with various states and we do the click on each.

Then we check which function is called.

Review Test Code

Test code should be looked at so that we know the intent of the developer quickly.

Conclusion

We should test simple behavior in our tests.

Also, we shouldn’t mock everything to have more realistic tests.

The post JavaScript Unit Test Best Practices — Testing Behavior appeared first on The Web Dev.

Top comments (1)

Collapse
 
ankitverma profile image
Ankit Verma

thanks for sharing