Javascript Unit Testing Best Practices to Follow
By Mohit Joshi, Community Contributor - November 6, 2024
Testing is a process of ensuring whether the developed product works as intended. A lot of tests are written in JavaScript. JavaScript provides top-notch features to make your tests more functional and less flaky. Implementing best practices in JavaScript testing enhances code reliability, maintainability, and performance. This guide lists the top best practices for JavaScript Unit Testing.
- Best Practices to follow in JavaScript Unit Testing
- 1. Don’t use “try…catch”
- 2. Don’t use mock
- 3. Write good test descriptions and use more scenarios
- 4. Don’t overuse helping libraries and test preparation hooks
- 5. Incorporate a suitable naming convention
- 6. Keep in mind the cross browsing aspect
- 7. Always use the BDD approach
Best Practices to follow in JavaScript Unit Testing
Effective unit testing in JavaScript ensures each function performs as expected, boosting overall code quality and confidence in deployment. These best practices help you create tests that are easy to maintain, quick to execute, and effective in identifying issues early.
1. Don’t use “try…catch”
The “try…catch” statement is used to catch errors made by the programmer. However, it is recommended not to use this approach of wrapping elements with the try-catch method. It is only good to catch errors that are predetermined, whereas a lot of cases are unpredictable. This method separates the program core’s logic from error handling logic, which makes it less reliable for testing purposes.
Let’s consider an example where you will create a function that checks if the two passwords are equal and throw an error when one password is left blank using the try-catch method.
it('shows error when first password is not given’, () => { try { isPasswordSame(null, "passWd"); } catch (error) { expect(error.message).toBe("password not found"); } });
In the above code, still passes our tests when the second password is not entered and left blank. To come up with a better approach, we can use toThrow assertions, which will help us frame a test where you will get the error when you don’t enter the second password.
it('shows error when first password is not given', () => { expect(() => isPasswordSame(null, "passWd")).toThrow("password not found"); });
2. Don’t use mock
Mocking is the process of introducing external dependencies on the unit that is being tested. It is done to bring focus to the unit that is being tested and not on the behavior of external dependencies.
Using mocks in your code is good. However, it is often overused. Do not mock everything, as it makes the tests slow. Mocks should only be used when there is less possibility of including dependencies on our tests, such as when testing HTTP requests.
Let’s understand with an example in which cases mocking is an effective solution.
function getTimeStamp() { const now = new Date(); const hours = now.getHours(); const minutes = now.getMinutes(); return ‘${hours}:${minutes}’ }
In the above example, the function requires time, however, it needs the current time, which keeps on changing. While testing, this will throw an error.
To resolve this you can use mocking and set an instance of time that will be used when you test.
beforeAll(() => { jest.useFakeTimers('modern'); jest.setSystemTime(new Date('31 October 2022 01:00 IST').getTime()); }) afterAll(() => { jest.useRealTimers() }) test('setting the formatted time', () => { expect(getTimestamp()).toEqual('14:32:19') })
For the purpose of the example, Jest is used. The same is the case with with other frameworks like Babel, TypeScript, Node, React, etc.
Also Read: Top Javascript Testing Frameworks
3. Write good test descriptions and use more scenarios
Writing proper test descriptions improves the readability of your code. The goal is to provide ample information while organizing them properly so the developers can quickly get to the section they want.
For instance, if the developer updates a method in the code, your proper structured code will guide them on which section of the test requires change now. Therefore, it is a good practice to always write test descriptions which improves the readability of the code.
Here are a few steps you can follow to make your code more structured:
- Use Describe block effectively while nesting
describe('<HomePage/>', () => { it('displays the user as a guest when not logged in', () => {}); it('prompts the user to login if not logged in', () => {}); it('user proceed as a guest when not logged in', () => {}); });
In the above example, the nested code doesn’t look well-structured, thus introducing a ‘describe’ block here will make it more organized.
describe('<HomePage/>', () => { describe('when user is a guest', () => { it('displays the user as a guest', () => {}); it('prompts the user to login/signup', () => {}); it(‘user proceed as a guest', () => {}); }); });
- Write detailed descriptions
describe('ShoppingApp', () => { describe('Add to cart', () => { it('When item is already in cart, expect item count to increase', async () => { // ... }); it('When item does not exist in cart, expect item count to equal one', async () => { // ... }); }); });
- Prevent duplication of code
It is obvious if you’re repeating code, it becomes less readable and maintainable. The goal here is to not repeat the implementation logic of your code.
4. Don’t overuse helping libraries and test preparation hooks
Helper libraries provide the comfort to work with complex setup requirements by installing all the implementation details automatically. However, extensive use of helper libraries creates confusion for developers, especially when they have not worked on your project very much. Also, this hides the underlying processes and configurations of your project.
The ultimate aim of your test must be to take the developer on a simplistic journey of code throughout the testing. This is easily achievable when you’re not using any helper libraries. It reduces the time for the developer to figure out what’s going on in the code and where to bring changes.
Apart from helper libraries, extensive use of test preparation hooks (beforeAll, beforeEach, etc.,) also brings complexity to your code. It creates confusion for developers in debugging.
Read More: Common Javascript Issues and its Solutions
5. Incorporate a suitable naming convention
Having a good structure of your code is one thing, and incorporating a suitable naming convention to them is another. Both go hand in hand and must be practiced mindfully. It is always worth assigning proper names to the scenarios used in your test.
The idea behind assigning a proper naming is to not leave any gap between you and your team members while collaborating on a project.
6. Keep in mind the cross browsing aspect
Your web application is going to be accessed by a wide range of browsers, therefore it becomes necessary that you adopt code in such a manner that is accessed in all the major browsers and operating systems flawlessly. You can use cross browser testing tools like BrowserStack to test your apps and websites on different desktop and mobile browsers.
7. Always use the BDD approach
BDD stands for Behaviour Driven Development and is an extension of TDD. The idea behind BDD is to fill whatever there is a gap in communication between all the stakeholders of the project. Following this practice allows more creative ideas to flow in the project which will ultimately improve the tests. BDD is written with the help of Gherkin language, which is simple English, however, each scenario written with Gherkin language following the BDD approach is binded to its necessary source code by a tester.
Also Read: BDD vs TDD vs ATDD
Here’s an example of how to follow the BDD approach.
Feature: Signin @smoke Scenario: Signin to bstackdemo website Given I open bstackdemo homepage And I click signin link And I enter the login details And I click login button Then Profile Name should appear
Conclusion
Start implementing these unit testing best practices to catch issues early, streamline development, and build reliable applications.
After the unit tests are executed, evaluate the next level of checks, like cross-platform testing on real devices. It helps to evaluate actual user scenarios more accurately. You can choose testing platforms like BrowserStack for real device testing, which provides a Cloud Selenium Grid with 3500+ real device, browser, and OS combinations.