DEV Community

John Au-Yeung
John Au-Yeung

Posted on • Originally published at thewebdev.info on

JavaScript Unit Test Best Practices — Organization

Subscribe to my email list now at http://jauyeung.net/subscribe/

Follow me on Twitter at https://twitter.com/AuMayeung

Many more articles at https://medium.com/@hohanga

Even more articles at http://thewebdev.info/

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.

Design for Lean Testing

Test code should be simple, short, and abstraction free.

They should also be lean.

The less complexity our test code has, the better it is.

We just do something and check the results in our tests.

Include 3 Parts in Each Test Name

We should have 3 parts in each test name.

The first part is the module being tested.

The 2nd is the circumstance that we’re testing the code in.

The expected result is what we’re checking for in our test.

This way, we can tell what’s being tested and find out what to fix if it fails.

For example, we write:

describe('User Service', function() {
  describe('Add new user', function() {
    it('When no password is specified, then the product status is pending', ()=> {
      const user = new UserService().add(...);
      expect(user.status).to.equal('pending');
    });
  });
});

Structure Tests by the AAA pattern

We should structure our tests with the AAA pattern.

The 3 A’s stands for Arrange, Act, and Assert.

Arrange means the setup code is added to let us do the test.

Act is doing the test.

Assert is checking the result after doing what we have.

So if we have:

describe('User Service', function() {
  describe('login', function() {
    it('if the password is wrong, then we cannot log in', () => {
      const user = new UserService().add({ username: 'james', password: '123456' });
      const result = login('james', '123');
      expect(result).to.equal(false);
    });
  });
});

We arrange with the first line of the test.

Act is the call for the login function.

Assert is the expect call.

Describe Expectations in a Product Language

We should describe tests in human-like language.

The behavior should be described so that we can understand what the test is doing.

The expect or should should be readable by humans.

For instance, we write:

describe('User Service', function() {
  describe('Add new user', function() {
    it('When no password is specified, then the product status is pending', ()=> {
      const user = new UserService().add(...);
      expect(user.status).to.equal('pending');
    });
  });
});

The strings we pass into describe and it explains the scenarios and tests clearly.

Test Only Public Methods

We should only test public methods so that we aren’t testing implementation.

We don’t care about the implementation of our tests.

All we care about is the results.

We should look at them when we don’t get the expected results from the tests.

This is known as behavioral testing.

We’re testing behavior and nothing else.

For example, we shouldn’t write code like:

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

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

We shouldn’t test private variables which can change any time.

Avoid Mocks in Favor of Stubs and Spies

We got to stub some dependencies since we can’t do everything in our tests as we do in a real environment.

Our tests shouldn’t depend on anything outside and they should be isolated.

Therefore, we stub all the dependencies that commit side effects so that we can just test what we want to test in isolation.

We just stub any network request code.

And we watch what we want to check is called with spies.

For instance, instead of mocking a database like:

it("should delete user", async () => {
  //...
  const dataAccessMock = sinon.mock(DAL);
  dataAccessMock
    .expects("deleteUser")
    .once()
    .withArgs(DBConfig, user, true, false);
  new UserService().delete(user);
  dataAccessMock.verify();
});

We spy on the function we check it’s called by writing:

it("should delete user", async () => {
  const spy = sinon.spy(Emailer.prototype, "sendEmail");
  new UserService().delete(user);
  expect(spy.calledOnce).to.be.true;
});

We spied on the Emailer constructor’s sendEmail method to check if it’s called after we called UserService instance’s delete method.

We avoid mock any server-side interactions with the spies approach.

Conclusion

We should test with stubs and spies.

The tests should be lean and divide our tests into 3 parts.

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

Top comments (0)