Code Refactoring: Why Is It Crucial For Software Testers
Software development involves one of the major key terms which is essential for the testers. This key term is known as Code Refactoring. We’ll take a closer look at this technique and how important it is to know. What are the benefits associated with code refactoring? And most importantly why testers need to know about it?
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. These 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 needs 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 tests required like load tests, security tests, 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 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 re-work as the feature evolves.
Being a developer of automation tests, automation test engineers also need to think like a developer and aim to create clean and easy to maintain code. Most of the IDE like IntelliJ IDEA, Eclipse, etc., include an in-built refactoring menu with commonly used refactoring methods for easy reference.
What Exactly is Code Refactoring?
Refactoring is the process of restructuring code, without actually modifying the original functionality of the code. Basically, software normally fails when it becomes so complex that it can no longer provide additional features while remaining error-free. Code refactoring is used to improve code design to make it easier to understand and extend. The goal of refactoring is to improve internal code by making many small changes without altering the code's external behavior.
If you believe a feature may be needed in the future and your current code won’t easily be able to accommodate it, you refactor that code to make it easier to add that feature now rather than worrying about what you need to change with your code to add the feature later.
Why is there a need for Refactoring?
Good programmers write code that other programmers can understand but also can change and maintain. This is what we should aim at in our Selenium projects.
Our code should also be easy to understand, change and maintain.
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:
1. Code smells
2. Technical debt
3. 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 more 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 classes or freeloaders.
· The existence of too many conditions and loops that have the potential to be broken down and simplified.
· Code builds 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 to 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 performance in extreme scenarios.
2) Technical Debt:
While developing 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 a test for the new feature. Approach A, which 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 makes the tests agreeable in short term.
If the team is under pressure to deliver the tests within a limited time, then they may agree to follow Approach B for now and add Approach A in the backlog for the future. By doing this, this team just created the technical debt for themselves.
In simple terms, technical debt in software testing or 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.
3) Following Agile Software Development Approach:
The agile software approach advocates incremental progress. Without clean, well-structured, and easy-to-maintain code, it would not be possible for teams to extend the existing code for additional tests with each iteration. If the code is changed without proper refactoring, then it may contribute to code smells or technical debt.
Let us look at some rules to write good code.
Rules to follow for writing good code
· Write Clean Code
The code should be clean. This means a lot of things.
1. The code should use clear names -Variables should have names that explain their purpose. Methods should have names that either explain what the method does or the result returned by the method.
2. Use problem domain names - If you do automation for an e-commerce site, make sure that you have classes for concepts such as
3. Classes should be small - A class should contain an average of less than 30 methods
4. Methods should be small
Methods should not have more than an average of 30 lines of code
5. Do one Thing - This applies to both classes and methods. If a method does more than one thing, consider splitting it in 2. If a class has more than one responsibility, consider breaking it in more classes.
6. Don’t Repeat Yourself - Any duplication, inside a page object class, a page method, a test class or a test method should be avoided.
7. Explain yourself in code - Write code that is self-explanatory, that is so easy to understand so no comments are needed.
8. Make sure the code formatting is applied - Code formatted correctly is easier to read by other developers.
9. Use Exceptions rather than Return codes - If a method cannot fulfill its purpose, instead of returning an obscure error code, throw an exception since the code is in an abnormal state.
10. Don’t return Null - There are many ways of avoiding returning null. You can use Optional introduced in Java 8. Or you can return an empty list.
· Write Secure Code
1. Make class final if not being used for inheritance
Making the class final ensures that it is not extended.
All page classes and page element classes should be final.
2. Avoid duplication of code
3. Limit the accessibility of packages, classes, interfaces, methods, and fields
Parent classes (base page class, base test class) should be abstract.
All methods of a parent class should be declared as protected since they should only be used in the child classes.
Only a small number of methods of any class should be public.
If a class should be used only by other classes in the same package, use the default access modifier.
4. Validate inputs (for valid data, size, range, boundary conditions, etc)
Any public method should have its parameters checked for validity.
5. Avoid excessive logs
If you are logging tracing information for all page classes and their methods, when you run the whole suite of tests, you will get excessive logs that are very difficult to read.
This becomes even worse if the automated tests are run in parallel on different virtual machines.
6. Release resources (Streams, Connections, etc.) in all cases
Consider using try/catch with releasing resources.
7. Purge sensitive information from exceptions (exposing file path, internals of the system, configuration)
8. Do not log highly sensitive information
9. Make public static fields final (to avoid caller changing the value)
10. Avoid exposing constructors of sensitive classes
Use a factory method instead of the constructor to create objects.
1. Use checked exceptions for recoverable conditions and runtime exceptions for programming errors
2. Favor the use of standard exceptions
3. Don’t ignore exceptions
4. Check parameters for validity
5. Return empty arrays or collections, not nulls
6. Minimize the accessibility of classes and members
7. In public classes, use accessor methods, not public fields
All fields of a class should be private so that objects cannot be changed
8. Minimize the scope of local variables
Declare the variable just before you need it and not at the beginning of the method.
9. Refer to objects by their interfaces
Also called program to an interface.
10. Adhere to generally accepted naming conventions
11. Use enums instead of integer constants
12. Beware the performance of string concatenation
13. Avoid creating unnecessary objects
This is obviously an incomplete list.
But it should get you started on focusing more on the quality of your code.
Possible Forms of Refactoring
1. Deleting Code is An Encouraged Form of Refactoring
Delete the sections of code such as
Functions that weren’t being called anywhere
After the cleanup operation, the next stage was to look at the “helpers” class. It can be easy when creating a generic framework to stick lots of functions into a helper’s section.
However, it’s important to break down the specific features you are delivering into the groups. Also, keep the classes small enough for people to find what they need easily. Without grouping your helpers, you can end up duplicating functions. This happens because they can’t find what they were looking for in a reasonable time.
2. Breaking Down Your Tests
Moving on from the framework refactoring, the next stage was to look at the large test classes. It’s important to remember where it’s best to place those edge case test scenarios within your test suite.
I know that we tend to be enthusiastic to include complex cases. It is recommended to keep the classes under 100 lines, maximum of 200.
When you are defining your test classes, they often start like the name of the page you are developing, as recommended in the Page Object Model. However, when the page gets rich with features, then you need to start thinking about breaking up your test classes. A couple of approaches include:
Positive and negative scenarios
By CRUD operations
3. Chopping Up Tests by Component
It is preferred to break tests down by component, aligning with the development approach used in frameworks such as React. By breaking it down to components you can isolate the tests, making them more stable and easier to maintain. Keep the test scenarios centered around the user and how they would use the application as a whole.
Again, don’t focus on testing all the ways you can interact with that specific component, but on how it fits into the core user journey. For example, let’s say you are setting up your donation page for your chosen charity.
The component you are moving to its own component is the date component- within the acceptance test suite, you focus on choosing a date in the future for your charity fun run. Scenarios like adding dates in the past or dates in 2 years’ time, or entering an invalid date can be covered at the unit level.
Let’s go over what we have covered regarding Code refactoring
Delete as much as you can before tackling any refactoring, as it might be duplicating your work when it can be just deleted it at the start
Don’t follow the page object model strictly, but adopt the right approach for your context. Test files can be broken down in different ways to ensure they are not overwhelming to read and easy to search for existing functions to reuse
Focus on the user journey, and implement your refactored tests with the user behavior in mind to ensure to retain the end-to-end coverage.