Understanding Web App Testing
4 September, 2024
12 mins
Understanding Web App Testing
When many of us started developing applications, we often did so without writing test cases, taking pride in our work and enjoying the process. However, upon joining an organisation or beginning a project, we quickly realised that including test cases is a necessity, and how it speeds up our workflow so we can develop software faster. Understanding why testing is crucial in application development can clarify why companies emphasise writing test cases.
As a developer or maintainer, it's important to understand that testing is not just a bureaucratic requirement; it is a fundamental part of ensuring the reliability and functionality of your application. Without understanding the what, why, and how of testing, one might be tempted to skip it altogether. However, by grasping the reasoning behind testing, we can appreciate its value.
What is testing
Testing is a process where the test program throws an error when the actual result of something does not match the expected output. The goal of any testing is that you try your best to make sure that something unexpected doesn't happen, or at least what the user is expecting works fine for sure.
It is important to know that testing never guarantees that all will be fine, but rather helps you assert that certain elements of your app will be working as expected based on how good your tests are. As a start, you focus on the part which your app can't do without like the “Add to cart” button is clickable and the cart being updated after it's clicked.
Why testing is important
Testing plays a crucial role in maintaining and improving code quality by catching bugs early. Testing enables developers to identify potential issues before they escalate, leading to a more robust and maintainable codebase. As software projects grow, they naturally accumulate complexity, and the ability to refactor code safely is paramount.
The key to productivity in software development lies in reducing cognitive load. When developers have fewer things to worry about, they can focus more on solving the actual problems at hand. Automated tests serve as a form of documentation, ensuring that certain conditions remain true as the code evolves. This provides the developers with the necessary confidence to write code more freely, knowing that their tests will alert them if anything goes wrong.
Testing is essential in software development because it not only ensures that your code functions as expected, but also simplifies the development process. It boosts productivity, facilitates team collaboration, and instils confidence in developers. By making testing an integral part of the development process, teams can ensure that their software is reliable, maintainable, and scalable.
What should you test
It is essential to recognise that testing isn't about covering every line or condition of your code. Instead of merely increasing code coverage, testing should prioritise the outcomes and assess whether the code fulfils its intended purpose, meeting both your expectations and those of the user.
When users interact with your app, they engage with the interface, not the underlying code. Therefore, your test cases should focus on verifying that user interactions behave as expected. When crafting test cases, the goal should be to evaluate the product's flow: Does the app function as intended? Are user actions, such as clicks or changes, triggering the correct responses?
Also, when writing test cases, one of the most frustrating aspects is having to update them every time the code changes. This often happens because the test cases are focused on the wrong part of the code—specifically, the implementation details. When tests are too tightly coupled with the underlying implementation, even minor code changes can necessitate extensive updates to the tests.
The more your tests resemble the way your software is used, the more confidence they can give you - Kent C. Dodds
The React Testing Library, created by Kent C. Dodds, was developed with this exact concern in mind. The library emphasizes the importance of testing the app's behaviour from the user's perspective rather than focusing on the internal workings of the code. By following this approach, you can create more resilient tests that remain stable even as the implementation evolves. You can read more about it here.
Levels of Testing
Different levels of testing—Unit Testing, Integration Testing, and End-to-End (E2E) Testing—address different aspects of the application's functionality. Together, they create a robust testing strategy that covers the application from individual components to full workflows.
Unit Testing
Unit testing focuses on testing individual components or “units” of code in isolation. The goal is to verify that each part of the application behaves as expected under various conditions. Unit tests make it easier to refactor code confidently, knowing that any issues in the individual components will be caught.
Integration Testing
Integration testing aims to test how different modules or components of the application work together. It ensures that the interactions between different parts of the application function correctly. It verifies that data is correctly passed between different parts of the application.
E2E Testing
E2E testing simulates real user scenarios and tests the entire application from start to finish. By mimicking user interactions, E2E tests help ensure that the application delivers a smooth and error-free experience to the end user. This type of testing ensures that the application behaves correctly in a real-world environment, covering everything from user input to the final output. By testing the full stack, E2E tests can identify issues that might not be apparent at the unit or integration levels.
Effective Strategy: The Testing Pyramid
A commonly recommended approach is the testing pyramid:
- Unit tests form the base of the pyramid with the most coverage and fastest execution time.
- Integration tests take up the middle layer, with moderate coverage.
- E2E tests sit at the top of the pyramid with fewer tests, focusing on core functionality and user flows.
This strategy ensures a balanced trade-off between coverage, speed, and thoroughness, offering an effective and scalable approach to web app testing.
The modern take on the testing pyramid is the testing trophy by Kent C. Dodds.
Source and Credit: Kent C Dodds's Blog
Types of testing
As we learned about different levels of testing, it's important to recognize that these are not the only types of testing. Several other testing methods play a crucial role in ensuring the quality and reliability of a web application. Some of these additional types of testing include:
Functional Testing
- Acceptance Testing: Confirms that the application meets business requirements and is ready for release.
- Smoke Testing: A quick check to ensure that the most critical functions of the application are working.
- Regression Testing: Re-tests existing features to confirm they still work after changes to the codebase.
Security Testing
- Penetration Testing: Simulates attacks to identify and fix vulnerabilities in the application.
- Security Testing: Assesses the application for common security vulnerabilities like SQL injection, cross-site scripting, and more.
Performance Testing
- Load Testing: Tests how the application performs under a high volume of users or transactions.
- Stress Testing: Evaluates the application’s behavior under extreme conditions, beyond normal operational limits.
- Scalability Testing: Checks how well the application scales with increased load.
- Endurance Testing: Tests the application’s performance over an extended period, to identify potential memory leaks or degradation.
Usability Testing
- Exploratory Testing: Testers explore the application to identify defects and usability issues without predefined test cases.
- Accessibility Testing: Ensures that the application is usable by people with disabilities, including those who rely on assistive technologies.
Compatibility Testing
- Cross-Browser Testing: Ensures that the application works consistently across different web browsers.
- Cross-Platform Testing: Confirms that the application functions well on different devices and operating systems.
- Localization Testing: Verifies that the application behaves appropriately in different regions, considering language, currency, and cultural differences.
Testing impact on code
Testing helps us ensure we don't break existing code when making changes. Testing also plays a critical role in helping us structure our code more effectively. By prioritising testability, we naturally improve the separation of concerns, making our code cleaner and more maintainable. However, it is important to remember that tests exist to describe the intention behind the system, not to dictate how the code is written.
Few practices you can follow to make code more testable, avoid doing real work in constructors—use them only to store passed-in dependencies. Control dependencies explicitly by using dependency injection, allowing for easy substitution with mocks during testing. Follow the Law of Demeter by passing only the necessary data to methods, reducing test complexity. Avoid global state to ensure tests are isolated and do not interfere with each other. Keep your code modular and simple, with each component having a single responsibility, and avoid closing over external variables. These practices lead to more reliable, maintainable, and easily testable code.
While these practices encourage writing good testable code, we should be cautious not to let tests influence the code in ways that hinder development. For example, it's essential to avoid tightly coupling the code with the tests, as this can make the system harder to change. Tests should verify the behaviour and outcome of the system without forcing the code into rigid patterns simply to accommodate the test.
By striking this balance, we ensure that testing enhances code quality without impeding progress or innovation in development.
Summary
Testing is a crucial aspect of web application development that ensures reliability, functionality, and user satisfaction. Here are the key takeaways:
- Purpose of testing: To verify that user interactions behave as expected and the application functions as intended.
- Testing approaches: Unit, Integration, and End-to-End (E2E) testing form a comprehensive strategy, often visualised as the Testing Pyramid or Trophy.
- Focus on behaviour over implementation: Write tests that are resilient to code changes by prioritising user perspective over internal workings.
- Testing improves code structure by encouraging separation of concerns, but should not dictate how code is written.
- Effective testing provides confidence in deploying applications and ensures a smooth user experience.
In our upcoming post, we'll dive into the nitty-gritty of crafting test cases. We'll explore the essential APIs and best practices for writing effective tests, and discuss strategies for organising your test suite. We'll also peek under the hood of popular testing frameworks, examining their core APIs and fundamental building blocks. This practical guide will equip you with the knowledge to write robust, maintainable test cases for your web applications.