Categories
Testing

JavaScript Unit Test Best Practices — Test and Automation

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.

Perform End to End Testing Over a Production-Like Environment

We should run our end to end tests over a production-like environment so that we know that our code will probably work in the production environment.

We can create an isolated environment with Docker-compose with one command.

It will create a Dockerized environment with everything we need with one command.

We can also use something like AWS Local to stub AWS services.

Parallelize Test Execution

We should run tests in parallel so that we can run more tests in less time.

There’re extensions for test frameworks like Jest and Mocha that’ll let us run tests in parallel.

Since we’re supposed to keep tests independent of each other, we should be able to do this easily.

We can run tests with multiple processes to speed up feedback time a lot.

Stay Away from Legal Issues

We should check for licenses so that we won’t be violating them.

Otherwise, if someone catches us, then we’ll run into problems.

It’s easy to use things accidentally without looking at the license.

We can use the license-check package to check our code.

To install it, we run:

npm install -g license-checker

Then we check the licenses of our dependencies by running:

license-checker --summary --failOn BSD

Inspect for Vulnerable Dependencies

We should inspect for vulnerable dependencies so that attackers can’t attack our apps.

To do this, we run npm audit or use snyk to check for vulnerable dependencies automatically.

Automate Dependency Updates

We can automate the update of our dependencies so that we won’t have to do everything ourselves.

Tools like ncu let us manage dependencies automatically.

There’s also the npm outdated to check for outdated dependencies.

We can run that in our CI to stop and builds that have obsolete dependencies.

Also, we can check each pull request for updated dependencies.

Native Docker Support

Docker makes devops easier, so we can use it to deploy and run our apps instead.

Fail Early

We should run our fastest tests first so that we can run quick inspections of our app.

And if there’re any failures, it’ll occur early.

Check Build Artifacts

Build artifacts like test and coverage reports, logs, etc. should be easy to find so we don’t have to waste time looking for them.

Create Multiple Pipelines

Pipelines are useful for automating our work.

We can configure a job for feature branch commits and different one for master PR.

But we can reuse the logic between them so that we don’t have to do the same work twice.

No Secrets

We shouldn’t have secrets in our jobs.

They should be retrieved from a secrets store from the job configuration.

Bump Version in Release Builds

The version in release builds should be bumped so that we can distinguish them.

Conclusion

There’re other things we can automate so that we can do less work.

Categories
Testing

JavaScript Unit Test Best Practices — Performance and Smoke Tests

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.

Watch How the Content is Served over the Network

We want to know how fast our content is served to users over the network.

To measure this, we can use tools like pingdom or Lighthouse.

They’re available as programs that we can add to our CI pipeline to ensure continuous monitoring.

They show us test results in various formats.

Stub Flaky and Slow Resources Like Backend APIs

If we’re running front end tests, then slow resources like back end APIs should be stubbed.

This way, we can run our front end tests as fast as possible.

We can stub them with various libraries.

This lets us simulate various API behaviors so that we can provide the required data for our front end.

Without the stubbed data, the tests will be slow and reliable.

For instance, we can write a test like:

test("show message when product doesn't exist", () => {
  nock("api")
    .get(`/products`)
    .reply(404);

  const { getByTestId } = render(<ProductsList />);
  expect(getByTestId("no-products-message")).toBeTruthy();
});

We stubbed the API call with nock so that we don’t have to make the actual API call.

Have a Few End-to-End Tests that Spans the Whole System

We should have only a few end to end tests that span the whole system.

They’re slow and so they should be reserved for testing the most critical parts of our system.

They simulate real user interaction so that we know that they’re acting right with user interaction.

They’re also brittle so it’s hard to run many of them.

Also, they should run in a production-like environment so that they’re testing something realistic.

Speed-up E2E tests by Reusing Login Credentials

We should just log in once and then do all our tests.

Logging in takes extra time so we should leave that for the beginning.

We can put the login code into a before all hook so that it runs before all tests are run.

Any records associated with the user should be generated with the tests.

We can save the auth token with Cypress, for example:

let authenticationToken;

before(() => {
  cy.request('POST', 'http://localhost:8888/login', {
    username: Cypress.env('username'),
    password: Cypress.env('password'),
  })
  .its('body')
  .then((res) => {
    authenticationToken = res.token;
  })
})

beforeEach(setUser => () {
  cy.visit('/profile', {
    onBeforeLoad (win) {
      win.localStorage.setItem('token', JSON.stringify(authenticationToken))
    },
  })
})

We get the username and password from the environment variables.

Then we log in with it and get the token by using the API instead of the GUI.

Then we get the token and use that before each test.

E2E Smoke Test that Just Travels Across the Site Map

End to end tests that just travel across the site makes sure that all parts of our site are working.

It’s easy to maintain and can find out any functional, network, or deployment issues.

Other kinds of smoke tests aren’t as reliable or exhaustive.

With Cypress, we can write:

it("can go to different pages", () => {
  cy.visit("https://example.com/home");
  cy.contains("Home");
  cy.contains("https://example.com/profile");
  cy.contains("Profile");
  cy.contains("https://example.com/about");
  cy.contains("About");
});

Conclusion

We can add various kinds of tests to test performance and smoke testing.

Categories
Testing

JavaScript Unit Test Best Practices — Organization

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.

Categories
Testing

JavaScript Unit Test Best Practices — Names and Expects

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.

Name Our Tests Properly

We should have concise, explicit, and descriptive names in our tests.

This way, we know what we’re testing.

Instead of writing:

describe('employee app', () => {
  it('returns an array', () => {
  });

  // ...
});

We write:

describe('employee app', () => {
  it('returns a list of employees when initialized', () => {
  });

  it('should calculate the pay of an employee when initialized', () => {
  });

// ...
});

We have the unit of work, scenario or content, and the expected behavior.

We can have them in this format:

describe('[unit of work]', () => {
  it('should [expected behaviour] when [scenario/context]', () => {
  });
});

or:

describe('[unit of work]', () => {
  describe('when [scenario/context]', () => {
    it('should [expected behaviour]', () => {
    });
  });
});

So we can write:

describe('employee app', () => {
  describe('when initialized', () => {
    it('returns a list of employees', () => {
    });

    it('should calculate the pay of an employee', () => {
    });
  });

  // ...
});

Don’t Comment Out Tests

We shouldn’t comment out tests.

If they’re too slow or produce false results, then we should fix the test to be faster and more reliable.

Avoid Logic in Our Tests

We should avoid logic in our tests.

This means we should avoid conditionals and loops.

Conditionals can make it take any path.

Loops make our tests share state.

So we shouldn’t write:

it('should get employee by id', () => {
  const employees = [
    { id: 1, name: 'james' },
    { id: 2, name: 'may' },
    { id: 3, name: 'mary' },
    { id: 4, name: 'john' },
    { id: 5, name: 'james' },
  ]

  for (const em of employees) {
    expect(getEmployee(em.id).name).toBe(em.name);
  }
});

Instead, we separate them into their own expect statements:

it('should get employee by id', () => {
  expect(getEmployee(1)).toBe('james');
  expect(getEmployee(2)).toBe('may');
  expect(getEmployee(3)).toBe('mary');
  expect(getEmployee(4)).toBe('john');
  expect(getEmployee(5)).toBe('james');
});

We have a clear output of all the cases.

We can also write out all the cases as their own test:

it('should sanitize a string containing non-ASCII chars', () => {
  expect(sanitizeString(`Avi${String.fromCharCode(243)}n`)).toBe('Avion');
});

it('should sanitize a string containing spaces', () => {
  expect(sanitizeString('foo bar')).toBe('foo-bar');
});

it('should sanitize a string containing exclamation signs', () => {
  expect(sanitizeString('funny chars!!')).toBe('funny-chars-');
});

it('should sanitize a filename containing spaces', () => {
  expect(sanitizeString('file name.zip')).toBe('file-name.zip');
});

it('should sanitize a filename containing more than one dot', () => {
  expect(sanitizeString('my.name.zip')).toBe('my-name.zip');
});

Don’t Write Unnecessary Expectations

We shouldn’t write unnecessary expectations.

If there’s the stuff that’s not used, then we shouldn’t add it.

For example, we can write:

it('compute the number by multiplying and subtracting by 2', () => {
  const multiplySpy = spyOn(Calculator, 'multiple').and.callThrough();
  const subtractSpy = spyOn(Calculator, 'subtract').and.callThrough();

  const result = Calculator.compute(22.5);

  expect(multiplySpy).toHaveBeenCalledWith(22.5, 2);
  expect(subtractSpy).toHaveBeenCalledWith(45, 2);
  expect(result).toBe(43);
});

We don’t need the spies since we’re testing the results of the computation.

So we can just write:

it('compute the number by multiplying and subtracting by 2', () => {
  const result = Calculator.compute(22.5);
  expect(result).toBe(43);
})

We just test the results since that’s what we care about.

We don’t want to check the implementation details.

Conclusion

We should test the results rather than the implementation details.

Also, we should name tests properly.

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.