Testing
Follow the testing pyramid
The testing pyramid is a guideline for determining the appropriate balance between different types of tests. It suggests that you should have more unit tests than integration tests, and more integration tests than end-to-end tests. This can help to ensure that you have a good balance of fast, reliable tests that cover a wide range of functionality.
In the testing pyramid, the tests can be divided into four categories: unit tests, integration tests, end-to-end tests and specialized tests.
Unit tests
Unit tests are tests that focus on individual units or components of the system, typically at the code level. They are often the most numerous and most granular tests in the pyramid. The goal of unit tests is to ensure that each unit of the system is functioning correctly on its own.
Integration tests
Integration tests test the integration between different units or components of the system. They are typically less numerous than unit tests, and they focus on how the units or components work together as a whole. The goal of integration tests is to ensure that the units or components can communicate and interact with each other correctly.
End-to-end tests
End-to-end (E2E) tests test the system as a whole, from end to end, by simulating real-world scenarios and interactions. They are typically the fewest in number, and they focus on the overall functionality of the system. The goal of end-to-end tests is to ensure that the system is working correctly from the user's perspective.
Specialized tests
Specialized tests are usually only run in specific circumstances and are not a core part of the testing pyramid. These tests are typically focused on specific areas of the codebase or specific types of issues, and they may not be necessary for every project. However, they can be useful in certain circumstances, such as when performance is critical or when accessibility is a requirement.
Here's a list of the different specialized tests you might encounter in a software project:
- Performance testing: Performance testing involves testing the performance and scalability of a system under different workloads. The goal of performance testing is to ensure that the system can handle the expected level of usage without experiencing performance issues.
- Security testing: Security testing involves testing the security of a system to identify vulnerabilities and ensure that it is secure. The goal of security testing is to protect the system and its users from potential security threats.
- Mutation testing: Mutation testing involves modifying the code being tested in small, controlled ways and seeing if the tests still pass. This helps to ensure that the tests are thoroughly covering the code and are not just passing because of some inherent weakness or oversight in the tests themselves.
- Accessibility testing: Accessibility testing involves testing the accessibility of a system for users with disabilities, including visual, auditory, and physical disabilities. The goal of accessibility testing is to ensure that the system is accessible and usable for all users, regardless of their abilities or limitations.
Follow a test coverage prioritization strategy
Test coverage prioritization can be useful in helping to prioritize testing efforts and ensure that the most important functionality is working correctly before moving on to less important scenarios. It can also help to identify and fix issues early in the development process, which can save time and effort in the long run.
There are various strategies that can be used to prioritize test coverage, such as focusing on the most frequently used or most critical functionality first, or identifying areas of the code that are most likely to be impacted by changes. The specific approach will depend on the needs and requirements of the project.
Even so, when following a test coverage prioritization strategy, some key rules to follow include:
- Focus on testing the most important features and functionality first. This may include features that are most used by users, have the most impact on the system, or are most critical to the business.
- Test the happy path, or the most common and expected use cases, before moving on to edge cases.
- Test the core functionality of the system before testing less important or peripheral features.
- Keep an eye on code coverage metrics, such as line coverage or branch coverage, and aim to achieve a high level of coverage for the most important parts of the codebase.
- Don't neglect testing of edge cases or less common use cases, but prioritize them based on their importance and potential impact on the system.
- Regularly review and update your test coverage prioritization strategy to ensure that it is still relevant and effective.
Use code coverage tools
Code coverage tools can help you to determine how much of your code is being tested by your test cases. This can be helpful for identifying areas of your code that may not have enough test coverage and for improving the overall coverage of your tests.
- IstanbulΒ (opens in a new tab): Istanbul is a popular code coverage tool that works with a wide range of JavaScript testing frameworks. It is easy to use and can generate coverage reports in a variety of formats.
- JestΒ (opens in a new tab): Jest is a popular testing framework that includes built-in code coverage tools. It can generate coverage reports in a variety of formats, including HTML and text.
- CoverallsΒ (opens in a new tab): Coveralls is a code coverage service that integrates with a variety of testing frameworks and build tools. It provides real-time coverage reports and can be used to track coverage over time.
- CodecovΒ (opens in a new tab): Codecov is a code coverage and analysis platform that supports a wide range of programming languages, including JavaScript. It provides detailed coverage reports and can be integrated with a variety of continuous integration and delivery (CI/CD) tools.
- BlanketΒ (opens in a new tab): Blanket is a simple code coverage tool that can be used to measure coverage. It can generate coverage reports in a variety of formats, including HTML and text.
Use a testing library or framework
There are many testing libraries and frameworks available for JavaScript that can help you to write and run tests. These tools can provide useful features such as test runners, assertions, and mock objects, which can make it easier to write and run tests.
- JestΒ (opens in a new tab): A popular testing library that provides a simple API for writing tests, as well as features for mocking and assertion.
- MochaΒ (opens in a new tab): A testing framework that provides a simple API for writing tests and support for running tests in parallel.
- ChaiΒ (opens in a new tab): A library for writing assertions in JavaScript, which can be used with any testing framework.
- JasmineΒ (opens in a new tab): A testing framework that provides a simple API for writing tests and support for mocking and spying.
- AvaΒ (opens in a new tab): A minimalist testing library that is designed to be fast and easy to use.
- TapeΒ (opens in a new tab): A lightweight testing library that is designed to be easy to use and fast to run.
- CypressΒ (opens in a new tab): An end-to-end testing tool for web applications that uses a real browser. It helps catch UI-related issues and test more realistic user scenarios.
- PlaywrightΒ (opens in a new tab): An open-source end-to-end testing tool by Microsoft that supports all modern rendering engines including Chromium, WebKit, and Firefox.
Write parallelizable tests
Whenever possible, try to structure your tests in a way that allows them to be run in parallel. This can help to speed up the testing process and to catch issues more quickly.
To make your tests parallelizable, try to keep them independent and avoid relying on shared state or globals. You may also need to consider how your tests will interact with any external resources or dependencies, such as databases or APIs.
Write tests for resilience and error handling
It is important to write tests that ensure that your code is able to handle and recover from errors and unexpected situations. This can help to improve the overall resilience and stability of your code. Here are a few tips for writing tests for resilience and error handling:
- Test that your code is able to recover from errors and continue running without crashing.
- Test that your code is able to handle and log errors, rather than ignoring them or failing silently.
- Write tests that simulate different error scenarios, such as network failures or invalid input, to ensure that your code is able to handle them properly.
- Consider using a testing library that provides built-in support for testing error handling, such as Jest with the
.toThrow
method. - Make sure to test for both expected and unexpected errors to ensure that your code is able to handle a wide range of error scenarios.
Use test doubles
Test doubles are fake objects that are used in place of real objects in tests. They can be helpful for isolating the code being tested and for controlling the behavior of the test environment. There are different types of test doubles, including mocks, stubs, spies, fakes, dummies and fixtures.
Mocks are test doubles that are configured to return specific values or behaviors when called. They are often used to test the interactions between different components or to verify that certain behaviors are triggered under certain conditions.
// Example of a mock
function DatabaseMock() {
this.calls = [];
}
DatabaseMock.prototype.get = function (key) {
this.calls.push({ method: "get", key: key });
return "mocked value";
};
DatabaseMock.prototype.set = function (key, value) {
this.calls.push({ method: "set", key: key, value: value });
};
It's worth noting that the definitions of these terms can vary somewhat depending on the context and the specific testing framework or library being used.
Put specific tests files next to the files they test
Placing test files for a particular module or component next to the code they are testing can make it easier to locate the tests and understand the connection between the code and the tests.
.
βββ src
βββ modules
βββ product
β βββ index.js
β βββ product.controller.js
β βββ product.model.js
β βββ product.test.js
βββ user
βββ index.js
βββ user.controller.js
βββ user.model.js
βββ user.test.js
Use test or tests directory for common tests and toolings
It can be helpful to create a dedicated test or tests directory for storing common tests and testing tools that don't particularly relate to any specific implementation file. This can make it easier to organize and maintain your common tests and testing tools, and it can also help to ensure that they are easy to find.
Use a test environment if necessary
Depending on the requirements of your app, it may be necessary to use a separate test environment for running your tests. For example, you may need to use a different database or set up mock data in order to properly test your app. In these cases, it can be helpful to use a dedicated test environment to ensure that your tests are accurate and reliable.
Test in different environments
It is a good idea to test your code in different environments, such as different web browsers or different versions of Node.js. This will help to ensure that your code is working correctly in all environments and will help to catch any issues that may be specific to a particular environment.
Use automated testing tools
Automated testing tools, such as test runners or continuous integration systems, can help you to automate the testing process. This can be helpful for running tests on a regular basis, such as after every commit, or for running tests in parallel, which can speed up the testing process.
Keep your tests up to date
It is important to keep your tests up to date as you make changes to your code. This will help to ensure that your tests are still relevant and are covering the most important functionality in your application.