We could almost agree that automated testing is the next best thing that happened to humanity since sliced bread. I won't evangelize testing in this blog post, but I aim to offer a word of caution about mitigating risks, particularly when you want to refactor code without the safety net of tests.
IDEs have undoubtedly evolved over the past decades; they have become highly reliable, capable of handling complex workflows, and offer impressive extensibility. Like any software, IDEs have their limitations and quirks. Most refactoring actions in IDEs have a proven track record. However, it is crucial to approach these transformations with caution, especially without adequate test coverage.
Trip hazard example: Extract function
The extract function/method transformation is a common refactoring often used during development. It's simple and works almost all the time.
On IntelliJ-based IDEs, you can do it in 3 steps.
- Select the expressions or statements you want to extract
- Perform the IDE's extract function action
- Give the newly extracted function a suitable name
The transformation works as intended in most cases.
Is it consistently safe?
"No".
The image shown above is very similar to the previous example. We have selected the desired expression to extract. And then, we extract!
If you look closer at line number 2 in Fig. 3 and Fig.4, you will notice that the variable car
's type was quietly
replaced from Car
to Unit
after extraction. That's a subtle bug that could have gone unnoticed without the IDE's type
hints in this scenario.
One way to resolve this problem is to ensure your selection includes the variable declaration and then perform the extract function refactoring.
So much better, this transformation did not change the program's semantics.
Refactoring without tests, the necessary evil
Statically typed languages, and IDE tooling for languages like Java and Kotlin is top-notch. Combining these factors with practices like micro-commits and ensemble programming makes it possible to make significant structural changes in these code bases without tests. Sometimes you may not have the luxury to write tests or may have to modify the code to bring it under test. In such situations, it is the developer's responsibility to make sure they don't accidentally introduce bugs.
Redundancies
The following are some of the redundancies I prefer to include when refactoring without tests.
- Pair with at least one more person in the team (pair/ensemble programming). Strong-style pairing works best.
- Bias toward IDE-assisted transformations.
- For manual transformations, derive proof from at least two other sources (compiler, IDE, linter, or VCS) and mark the commit as such. Arlo Belshee's commit notation has a set of prefixes you can use.
- Use micro-commits and review each commit with your partner—during and after making the changes.
- Test the code paths manually between the base commit and the
HEAD
. - If you have a branch-based workflow, get the changes in quickly.
These are a few, and you may have to consider redundancies based on your team, workflow, and available tooling.
Extra
Enable the Inlay Hints settings in your IDE to display type information directly in the code editor. This feature will provide real-time annotations that show the inferred types of variables, expressions, and function return values, making it easier to understand the code and ensuring that any potential type-related issues are identified and addressed promptly during refactoring. This enhanced visibility of type information can significantly improve the quality of refactoring in Kotlin projects.
Conclusion
In software development, the need to refactor code without tests may arise from time to time. While automated tests remain essential, IDEs, compilers, VCS, and linters offer valuable support for refactoring. However, we must stay cautious as IDEs may have limitations and occasional bugs. To mitigate risks, verifying transformations from alternative sources, using redundancies such as ensemble programming, and double-checking changes through micro-commits, peer reviews, and manual developer testing is crucial. Refactoring without tests demands vigilance, but by combining IDE capabilities with careful practices, we can navigate this challenge and craft stable, maintainable code that adapts to the evolving software landscape.