Testing is a deep topic, involving a wide variety of testing approaches at different levels of an app. We visualize testing as a pyramid, and we'll explore each layer in this guide.
Start and the bottom and work up. That is, if static analysis is not yet implemented, start there by implementing linting with each commit, disallowing commits that do not pass. Then move up to unit tests, etc.
The area of each area is a rough estimate of both the value of the tests as well as the overall volume of the tests.
Moving up the pyramid increases the following items: cost, complexity, difficulty of maintenance / fragility.
This layer refers to the use of linting tools or static analysis tools like Sonarqube to analyze the code and look for common issues and violations of best practices. Especially in the case of tools like linters it is very easy to run the tool with every commit. 100% coverage of the code should be very easy to achieve.
Unit tests focus on testing individual components, page, service, and other code modules in isolation ensuring that the module under test meets the requirements for the module. Key aspects of unit tests:
- individual code modules are tested in isolation, all dependencies are mocked, so if component A is being tested and it uses service B, service B is mocked
- tests are developed to determine where the module does not meet the requirements
- 100% coverage of the requirements is the target, often leading to 100% code coverage
Unit tests are generally run continuously during development, as well as with each CI/CD build.
In Angular, testing is built right into the official toolchain. Follow the Angular Testing docs to learn more. In React, testing may or may not be built into your project depending on whether it used create-react-app or something else. Follow the React Testing docs to learn more.
Integration tests are technically similar to unit tests, often using the same tools and libraries with the key difference being that only external dependencies are mocked. So if component A is being tested and it uses service B, service B is not mocked like it would be for a unit test. However, if service B makes HTTP calls to an API that is an external dependency and those HTTP calls are mocked.
Integration testing focuses on the interaction between modules rather than requirements or code coverage.
End-to-end testing exercises the application as a whole as opposed to individual modules. It is difficult to achieve full coverage of requirements or code.
End-to-end testing focuses on various user facing scenarios and workflows. For example:
- logging in to the app
- opening your transaction list
- entering a new transaction
With most Ionic Framework applications, automated end-to-end testing is performed using popular web-based testing tools such as Cypress or Selenium in order to exercise the application. The majority of a typical Ionic app is web-based, so these tools work well for testing them.
Automated device testing generally is performed by running end-to-end tests on a device farms. Automated device testing should focus on scenarios that are focused on native interactions. Some popular services here are AWS Device Farm, Sauce Labs, and Browserstack.
No matter how much automated testing is in place, there is no replacement for getting the application into the hands of human beings to run the application. Common forms of manual testing include QA regression testing prior to release, and acceptance testing performed by key consumers of the application.