- Reflection on TDD flow (Percival, 2017): Following the TDD workflow was useful because it forced me to think about the expected behavior of the system before writing the implementation, which kept my focus on correctness and design clarity. However, I noticed that sometimes I rushed into writing tests without fully considering edge cases or alternative scenarios, which limited the coverage of my tests. Next time, I need to spend more time upfront analyzing requirements and brainstorming possible failure modes so that my tests can anticipate more real-world situations and guide the implementation more effectively.
- Reflection on F.I.R.S.T. principle: The unit tests I created mostly followed the F.I.R.S.T. principle: they were fast to run, independent of each other, repeatable with consistent results, self-validating through clear assertions, and timely since they were written alongside the code. That said, a few tests could be improved in independence and clarity—for example, avoiding reliance on shared mutable state or making assertions more explicit about expected outcomes. Next time, I will ensure stricter isolation between tests (e.g., resetting data structures fully) and write assertions that communicate intent more clearly, so the tests remain robust and easy to understand.
-
- by creating distinct controllers (CarController and ProductController). Each controller now focuses solely on its own domain, managing HTTP requests, preparing models, and returning views specific to that entity. This separation ensures that controllers adhere to the Single Responsibility Principle (SRP), since they only deal with request handling and not business logic. By depending on service interfaces rather than concrete implementations, the controllers also comply with the Dependency Inversion Principle (DIP), making them flexible and open to extension without modification (Open/Closed Principle, OCP).
- In the service layer, we introduced CarService as an interface and implemented it in CarServiceImpl. This design enforces a clear contract for car-related operations, while allowing multiple implementations if needed. The service delegates persistence concerns to the CarRepository abstraction, ensuring that business logic is isolated from data access. This structure strengthens SRP (services only handle business rules), Liskov Substitution Principle (LSP) (any repository implementation can be swapped without breaking the service), and DIP (services depend on abstractions, not concrete classes). Together, these changes make the codebase more maintainable, testable, and aligned with SOLID principles.
-
Applying the SOLID principles to our project makes the codebase cleaner, easier to maintain, and more flexible. For example, in the controller layer we separated CarController from ProductController, ensuring each controller only handles requests for its own domain. This follows the Single Responsibility Principle (SRP) by keeping concerns isolated, and the Open/Closed Principle (OCP) because adding a new controller (like BikeController) can be done without modifying existing ones. In the service layer, we introduced the CarService interface and implemented it in CarServiceImpl, which delegates persistence to the CarRepository abstraction. This design enforces Dependency Inversion Principle (DIP) since the controller depends on the service interface rather than its implementation, and it respects the Liskov Substitution Principle (LSP) because any repository implementation can be swapped without breaking the service. Together, these changes make the system more testable, scalable, and aligned with SOLID best practices.
-
Not applying the SOLID principles to a project often results in tightly coupled, fragile code that is difficult to maintain and extend. For example, if both car and product logic were placed inside a single controller, you would violate the Single Responsibility Principle (SRP), making the controller bloated and prone to errors whenever changes are introduced. This also breaks the Open/Closed Principle (OCP), since adding a new entity like bikes would require modifying the existing controller instead of simply extending the system. Similarly, if the controller directly depended on CarServiceImpl or even the repository instead of an interface, you would violate the Dependency Inversion Principle (DIP) and the Liskov Substitution Principle (LSP), making it impossible to swap implementations without rewriting large portions of the code. As a result, switching from an in-memory repository to a database-backed repository would ripple changes across multiple layers, making testing harder, reducing flexibility, and increasing the risk of introducing bugs.
- When I was looking at my coverage score on the Jacoco index.html, I noticed that my score was low. So I made a dozen of test to make sure my coverage score was better. The strategy that I use was making the test according to the Jacoco index.html. In there I make tests for each method that hasn't been tested.
- Yes I think so !, because I make sure that my CI/CD implementation will always check, rebuild, test for every push or pull attempt. For now I believe that my CI/CD is enough. But maybe in the next few weeks, I would have new problems and I should make a better CI/CD implementation.
In implementing the Edit Product and Delete Product features using Spring Boot, several clean code principles were applied throughout the project. The code follows a clear separation of concerns by dividing responsibilities across controllers, services, repositories, and models, which improves readability and maintainability. Meaningful class and method names were used to reflect their purposes, such as ProductController, ProductService, and repository interfaces for data access. Reusable logic was placed in the service layer to avoid duplication, and consistent formatting and naming conventions were maintained across the codebase to enhance clarity.
From a secure coding perspective, the application avoids exposing internal implementation details by encapsulating business logic within the service layer and interacting with data only through repositories. Input handling is managed through controller methods instead of direct data manipulation, reducing the risk of unintended behavior. One improvement that can be made is adding stronger validation for user inputs (such as preventing empty product names or invalid quantities) and providing proper error handling or feedback to users when invalid data is submitted. Adding validation annotations and exception handling would further improve robustness, security, and user experience while keeping the code clean and maintainable.
- After writing the unit tests, I felt more confident about the correctness and stability of the implemented features. Unit tests help verify that each method behaves as expected in both positive and negative scenarios, making future changes safer and easier. There is no fixed number of unit tests required for a class; instead, tests should be written to cover all important logic paths, edge cases, and possible failure scenarios. To ensure that unit tests are sufficient, developers can use code coverage metrics, which show how much of the source code is executed during testing. Code coverage tools help identify untested areas that may contain hidden bugs or unverified logic.
However, achieving 100% code coverage does not guarantee that the code is free of bugs or errors. Code coverage only measures which lines or branches are executed, not whether the logic is correct or whether all real-world scenarios are handled properly. A test can execute a line of code without validating its correctness. Therefore, unit tests should be well-designed, include meaningful assertions, and be complemented by other types of testing such as integration and functional tests. Good test quality, not just high coverage, is essential for building reliable and maintainable software.
- Creating another functional test suite with the same setup procedures and instance variables as existing tests can lead to code duplication, which negatively impacts code cleanliness and maintainability. Repeating the same WebDriver setup, base URL configuration, and test initialization logic across multiple functional test classes violates the DRY (Don’t Repeat Yourself) principle. This duplication makes the code harder to maintain because any change to the setup logic (such as modifying the base URL or browser configuration) would need to be updated in multiple places, increasing the risk of inconsistencies and errors.
To improve code cleanliness, common setup logic should be extracted into a shared abstract base class or a reusable test utility that all functional test suites can extend or use. This approach reduces duplication and improves readability by allowing each test class to focus only on test behavior rather than infrastructure code. Additionally, using helper methods for common user interactions (such as creating a product or navigating to the product list) can further improve clarity and reusability. These improvements enhance maintainability, reduce technical debt, and ensure that adding new functional tests does not degrade overall code quality.