Originally published on Medium at Python in Plain English publication.
In a recent project, I was tasked with delivering a crucial backend component with robust functionality as well as requiring comprehensive testing. Initially, I perceived testing as a responsibility of the QA or automation team rather than mine as a Software Engineer; however, I soon realized that incorporating that testing into my development process was pivotal for ensuring code quality and resilience.
This article aims to provide insights into my approach towards testing as a backend engineer. By focusing on the mindsets and architectural strategies that empower developers to craft thorough testing, you can instil confidence in their codebase.
Understanding Backend Component Architecture
At the core of backend development lies the objective of processing inputs, executing calculations, and producing an output. For instance, consider a scenario where an API endpoint receives a pizza order with specified toppings, calculates the total bill based on the various parameters, and returns the result to the client; this exemplifies a basic workflow within a backend component.
Layered Structure: Controller, Service, and Data Access
To organize the backend component effectively, I adopt a three-layered architecture:
Controller Layer: Utilizing Express, this layer handles incoming requests and delegates tasks to service methods. Controllers define endpoints and facilitate communication with the service layer which helps keep routes concise and focused.
Service Layer: Comprised of classes responsible for business logic, the service layer interacts with data models and orchestrates various operations. Importantly, service methods do not directly interact with request or response objects, ensuring separation of concerns and promoting modularity.
Data Access Layer: Represented by data models implemented with frameworks like Objection, this layer facilitates interaction with the database. It encapsulates functionalities related to data retrieval, manipulation, and validation which maintains the integrity of data operations.
Building Comprehensive Tests
The primary goal of testing is to validate the functionality of each component and ensure that it behaves as expected under different scenarios. I categorize tests into True Positive (TP), False Positive (FP), and error scenarios to cover a wide range of use cases and edge conditions of function tests.
For every function within each layer, I develop three types of tests:
True Positive (TP): This evaluates the function’s correct behavior under valid input conditions. In the scenario of the pizza ordering system, a TP test would ensure that when a valid pizza order with all required toppings is submitted, the system would calculate the total cost accurately and return the expected result.
False Positive (FP): This is used to identify potential issues or unexpected behavior when an invalid input is provided. Continuing the pizza ordering example, an FP test might involve submitting an order with an invalid topping combination or missing required information. This test ensures that the system appropriately handles such scenarios and then perhaps rejects the order or provides an error message.
Error Handling: Testing various error scenarios, such as file not found or database connection errors, verifies the robustness of error handling mechanisms. In the pizza ordering system, an error handling test could simulate a situation where the database containing topping information is temporarily unavailable. This test ensures that the system gracefully handles the error, perhaps by displaying a user-friendly message or retrying the operation after a brief delay.
Additionally, unit tests should be conducted to verify the behavior of individual input and output fields to ensure data consistency and accuracy. For example, unit tests for the pizza ordering system might validate that the price calculation function accurately computes the total cost based on the selected toppings and any applicable discounts.
Conclusion
By integrating comprehensive function testing practices into the software design architecture, developers can fortify their components against bugs and uncertainties, fostering confidence in the codebase. Embracing this approach not only improves code quality but can also enable a smoother development process.
Happy coding!