All posts
Jul 25, 2023 in Refactoring4 min
Refactoring without tests: Mitigating trip hazards
Posted by
Ragunath Jawahar
Ragunath Jawahar
@ragunathjawahar

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.

  1. Select the expressions or statements you want to extract
  2. Perform the IDE's extract function action
  3. Give the newly extracted function a suitable name
Fig. 1: An expression selected for extraction
Fig. 1: An expression selected for extraction
Fig. 2: force function—extracted and named (Cmd + Option + M or Ctrl + Alt + M)
Fig. 2: force function—extracted and named (Cmd + Option + M or Ctrl + Alt + M)

The transformation works as intended in most cases.

Is it consistently safe?

"No".

Fig. 3: An expression selected for extraction
Fig. 3: An expression selected for extraction

The image shown above is very similar to the previous example. We have selected the desired expression to extract. And then, we extract!

Fig. 4: The extract function transformation failing silently by returning Unit
Fig. 4: The extract function transformation failing silently by returning Unit

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.

Fig. 5: Workaround for extract function
Fig. 5: Workaround for extract function
Fig. 6: car Function—extracted and lazily named
Fig. 6: car Function—extracted and lazily named

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.

Fig. 7: IntelliJ IDEA's inlay hints settings
Fig. 7: IntelliJ IDEA's inlay hints settings

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.

Overall, how helpful was this article?
😭🙁🙂😍

Stay ahead of the curve! 🚀

Subscribe now and never miss our cutting-edge, innovative content.
Address
Legacy Code Headquarters (OPC) Private Limited,
L-148, 5th Main Road,
Sector 6, HSR Layout,
Bengaluru, Karnataka-560102,
India.
© 2023–2024 Legacy Code HQ. All rights reserved.