Understanding Code Refactoring: A Tester’s Perspective
The term ‘Refactoring’ is mainly used to indicate required code cleanup/redesign.
In this tutorial, we will understand the definition of refactoring, discuss the need for code refactoring, and review the impact of refactoring code on various project team members. We will also discuss the answer for the most important question – As a tester, why do you need to know about refactoring?
Additionally, we will also discuss a few case studies to clarify the concept.
What You Will Learn:
Introduction to Refactoring
To start with, let us first understand, what actually refactoring is.
Refactoring is essentially a practice or process of improving the code and/or database while maintaining the existing functionality. The idea is to transform inefficient and the over-complicated code to more efficient, preferably simpler and easier code.
Refactoring of code has also picked up momentum with more teams now by following the Agile Software Development approach. Project teams often have limited time to implement new or extend the functionality of the existing features and code that is clean. The code which is easy to understand and maintain certainly goes a long way in meeting the iteration deadline.
Need for Code Refactoring
If we are maintaining the original functionality of the application or module, a question arises as Why do we even bother refactoring? Well, there are numerous reasons for which a particular module or piece of code may need to be refactored, like:
- Code smells
- Technical debt
- Agile software development approach, etc.
We will discuss these points in detail in the following sections.
#1) Code Smells:
We all can understand that when the food starts to smell it indicates that it is most likely turning bad – this is true for code as well! Code smells are indications that a much serious problem may exist in the code.
Following are some common code smells:
- Presence of redundant or identical code.
- A declared variable that is not used anywhere in the rest of the code.
- Overcomplicated code design.
- Code class that does too little and does not justify the existence of the class defined. Such classes are known as lazy class or freeloader.
- The existence of too many conditions and loops that have the potential to be broken down and simplified.
- Code build in a way that a change in one part of code requires the change to be implemented at the other places as well.
Code smells become more apparent with passing time. As the application or system grows, eventually these code smells start affecting the code development, maintenance and even system the performance in extreme scenarios.
#2) Technical Debt:
While developing a software, during the limited time and resources available, often we may take shortcuts to achieve the desired results.
Consider a feature that needs to be added to an existing module. After discussion, the team narrows down 2 approaches to add this feature. Approach A, takes 2 sprints to deliver, will be the approved long-term approach. Approach B takes only 5 days to deliver is a messy hard-coded hack that is designed to just service the module in short term.
If the team is under pressure to deliver the feature within a limited time, then they may agree to follow Approach B for now and add Approach A in the backlog for future. By doing this, this team just created the technical debt for themselves.
In simple terms, technical debt in software development refers to the additional rework or overhead required to put the appropriate fixes in place or do things in the ’right way’.
Legacy Systems tend to acquire huge technical debt over time which in turn may make the application susceptible to failure and difficult to support and maintain.
Read more => What is Technical Dept
#3) Following Agile Software Development Approach:
Agile software development approach advocates incremental development. Without clean, well-structured and easy to maintain code, it would not be possible for teams to extend the existing code with each iteration. If the code is changed without proper refactoring, then it may contribute to code smells or technical debt.
This relationship is depicted in the diagram below
Recommended Read => Top Code Analysis Tools
Why a QA Needs to Know About Refactoring?
Whatever we discussed until now in this tutorial seems to be related to coding and looks like the kind of things that a developer should worry about.
Then, why are we discussing this unrelated concept in Software Testing Help Community? Continue reading the rest of this tutorial for the answer to this question.
#1) For Unit Testers/Developers
While refactoring the code, as new code is being inserted, old classes are being updated, new classes are being added, and existing Unit tests may now fail. Additionally, for legacy systems, there may be no unit tests implemented at all. This new unit tests will need to be created and set up from scratch in a majority of cases.
#2) For Testers
When a feature is being refactored (considering that we are not adding any new functionality), the understanding is that after the required changes are done, a majority of the functionality for the end-user should remain the same.
- As a tester, refactoring of code roughly translates to = in-depth testing + regression testing. In-depth testing need to include all the existing user flows to ensure that all functionalities are working as before. Regression testing of the entire application (or impacted areas) is required to ensure that upgrading a module did not unintentionally break the functionality of the other modules.
- User acceptance tests will be important and these tests need to pass before the build can be declared ready for release.
- Additionally, any other test required like load tests, security test etc. would also need to be implemented as required.
#3) Automation Test Engineers
Refactoring of code may cause functional and non-functional automation scripts to fail.
This may occur due to the following reasons:
- If the page objects change as part of the refactoring effort and if your Selenium automation scripts rely on the page objects, then the scripts will fail and would need to be updated.
- If there were are minor changes, then it redirects the ones that were added or removed during refactoring, and existing automation scripts would fail and would need to be updated
It is recommended that the functional automation tests should only be set up once a feature is stable, otherwise it will result in a lot of rework as the feature evolves.
Being a developer of automation tests, automation test engineers also need think like a developer and aim to create a clean and easy to maintain code. Most of the IDE’s like IntelliJ IDEA, Eclipse etc., include in-built refactoring menu with commonly used refactoring methods for easy reference.
Below is a screenshot of the refactoring menu in IntelliJ IDEA (Community Edition).
#4) For Test Leads/ QA Leads
- Test Leads / QA Leads may be required to work together with the rest of the team including developers, Product analyst and maybe even stakeholders to ensure that Test planning for such projects is done carefully.
- It is important to understand the existing functionality. Based on the existing functionality, business cases, user flows and user acceptance tests need to be documented. When a refactored code is being tested, all these scenarios need to be validated, along with regression testing of the impacted areas.
- Be proactive while planning the test approach and test plans. If you anticipate the requirement of multiple test environments or new test tools, please send the requisition early to prevent any delays once the testing phase starts.
- Do not hesitate to involve non-project team members or end users to contribute to testing.
We will now discuss some real-life case studies in which I had the opportunity to either work on directly or contribute to indirectly.
Case Study #1
Task: Refactor a module to replace the hard-coded values with variables and add comments for new automated technical documentation generation tool.
Challenges: No major challenges. The module was new, had documented functional and user acceptance requirements, user flows and test cases. Unit tests were set up during the initial launch itself.
Test Approach: The test cases for the module being refactored and the relatively known impacted areas were executed. Any defects were reported and resolved before the release.
Case Study #2
Task: Refactor existing stored procedure to facilitate application scale up.
The stored procedure in this scenario was an older stored procedure that was designed a few years ago, keeping in mind the requirement that the application was using its stored procedure as an internal application with less than 10 concurrent sessions.
Now the company wanted to market this application as a Software as a Service (SaaS), with the expected volume of 300 concurrent sessions initially.
The team did some initial load tests and concluded that the system breaks with a load of 25 concurrent sessions. The team reviewed the code and recommended refactoring one existing core stored procedure that would allow the application to scale up and support up to 500 concurrent sessions without breaking.
Some issues identified with this stored procedure were multiple subqueries within a single query, heavy joins with views instead of tables, use of select * instead of selecting a specific column, etc. Due to these coding issues, the application was fetching more data than that was really necessary, thereby causing the application to slow down and eventually crash.
#1) Project Manager/Product Analyst
- Requirement Gathering – Since this stored procedure was a legacy code, there were no documented requirements for it when the module was first designed. Also for the iterations done over the last few years, there was no change log to indicate the business rules and logic added or removed from the module.
- Project Schedule – Since the requirements were not clearly defined and code dependencies were not yet fully identified, it was difficult to communicate the tentative schedule.
#2) For Developers
- Lack of clear requirements and business rules.
- Cleaning the code without losing its functionality.
- Unknown impacted areas and/or code dependencies.
- Unable to provide concrete development time estimates.
- Need to create new Unit Tests.
#3) For Testers
- Lack of clear requirements and business rules impact test planning.
- Unknown impacted areas impact test planning, specifically for regression tests.
- Unable to provide concrete testing estimates.
- Lack of clear documented requirements and/or user flows + tight deadlines = Higher risk of failure.
The approach followed by the team to mitigate risks and work around the challenges:
(i) The team followed a collaborative approach to gather requirements: Project Manager/ Product Analyst and Testers worked closely with the internal end users to understand and document the major functionality and user flow.
Developers also reviewed the code and added relevant information to the requirements document. This was done before the sprint start day.
(ii) The alternate test environment was created to test the change being implemented: Lets’ call these environments as Gamma and Phi. Gamma would have the old code and Phi would have the latest refactored stored procedure deployed at all times.
Having 2 test environments in parallel, almost recreating before – and – after approach, allowed the team to do more in-depth and exploratory testing by comparing the behavior in these 2 test environments, they were able to identify the probable bugs.
The team provided the requirements for an alternate test environment that was provided before the sprint start date.
(iii) End users and stakeholders involved in testing early on: Any obvious issues were caught and reported early on allowing more time for the team to deploy and test the required fix.
(iv) Defining exit criteria and adhering to it: Exit criteria were defined in the initial planning stages – 80% user flows tested, no critical bug unresolved, demo and sign off from the stakeholders before release.
(v) Setting a tentative release date: Setting a release date aligned and motivating the team to work towards a common endpoint. Based on the scope of the project, it was recommended by the team that a 3-week sprint be followed instead of a regular 2-week sprint to allow sufficient time for the team to execute the project.
To summarize, refactoring of code is a process to clean/simplify the design of a module without changing its functionality. The refactoring process can be simple, like adding comments, adding correct indentation, removing a static variable, etc. or can be complicated for complex legacy systems.
A particular module or piece of code may need to be refactored because of code smells, technical debt or by following an agile software development approach.
For testers, refactoring of code roughly translates to = in-depth testing + regression testing.
In-depth testing is required to test all the existing user flows to ensure if all functionalities are working as before. Regression testing of the entire application (or impacted areas) is required to ensure that upgrading a module did not unintentionally break the functionality of the other modules.
Test Leads / QA Leads may be required to work together with the rest of the team including developers, Product analyst especially for legacy projects. Be proactive while planning the test approach and test plans. If you anticipate the requirement of multiple test environments or new test tools, then please send the requisition early on to prevent any delays once the testing phase starts.
About the Author: This informative tutorial is written by Neha B. She is currently working as a Quality Assurance Manager and is specializes in leading and managing In-house and Offshore QA teams.
Have you worked on a challenging project which involved refactoring of code or a module/application? If yes, feel free to share your experiences in the comments section for our fellow testers to learn from.