The conventional wisdom is that you should have good test coverage before refactoring your code. Making mistakes during refactoring will introduce defects in the software if they are not caught during testing. Such regression defects can annoy users, break the software and even lead to customers abandoning the product.
The need for automated testing is especially high for JavaScript. In a dynamic language with functional constructs like JavaScript, it is harder to detect breakages statically. Additionally, JavaScript has many language details and quirks that are not well known or easy to remember.
In practice, however, there is a chance that you will encounter situations where you need to work in untested or insufficiently tested legacy code. Often this is precisely the kind of code that would benefit from refactoring to make it easier to understand and extend. But to test it, we need to make it testable, which involves refactoring.
How do we resolve this problem? If we take a step back, the underlying question that automated testing helps us answer is a question of confidence and risk:
How confident am I that my refactoring has not changed the program behavior?
Tests help us answer the question, but there is a chance that refactoring introduces bugs when there are gaps in test coverage. Therefore the answer to the question is a matter of degree, even with tests.
Another approach is taking a sequence of small steps. If we are confident that each step is correct, the final result is correct since no step changes the program behavior. Ideally, taking small, low-risk steps is combined with excellent test coverage, leading to high confidence.
However, significant changes, a more complex programming language like JavaScript, and complex legacy codebases lead to a high risk that individual steps introduce defects. In particular, refactorings that span module or service boundaries, large-scale renames where not every symbol occurrence might get picked up, or extensive manual changes tend to be error-prone.
But for single functions, classes or modules, it can be possible to chain together sequences of automated refactorings to achieve a more extensive refactoring. However, most refactoring tools ensure that the refactoring mechanics are correctly executed, but they do not tell you about the impact on your code.
I wanted a refactoring tool with built-in knowledge of many hard-to-remember JavaScript details that can analyze the relevant code to evaluate the safety of a refactoring. With this in mind, I created the P42 JavaScript Assistant for Visual Studio Code. When possible, P42 evaluates the refactoring impact on the logic and the flow of your code (excluding performance) and informs you if it is safe or if specific details need checking.
Here is an example of how P42 indicates the safety of small refactoring steps:
💡 This blog post shows P42 JavaScript Assistant v1.46
With automatic refactoring tools such as P42 that combine static analysis of the relevant source code with extensive knowledge of the programming language, it is possible to refactor code more safely. In combination with automated testing and type checking tools like TypeScript, this makes it possible to refactor with high confidence even in a dynamic language like JavaScript.
Happy Refactoring!
Top comments (7)
Thank you for sharing this! P42 JavaScript Assistant seems like an interesting tool to use. I still do believe that test coverage is important, however.
I agree! To me, test coverage, type checking and automated refactoring tools with safety checks all work together to lead to high refactoring confidence.
Yes, I do it all the time
Nice article. I have recently discovered writing functional JavaScript combined with unit tests has proven a game changer in terms of reduced bugs and confidence in re use of functions.
When you say automated testing, what do you mean?
Any level of automated test that would improve your confidence that you code is still correct and behaves as expected after the refactoring. This can include unit tests, but also higher level tests like integration or acceptance, and even performance and fault injection testing.
Great thanks. Automated meaning somewhere along the CI/CD to run the tests.
Yes, ideally pre-merge. It could also be on your machine. Usually different levels of testing are run at different stages due to their cost / runtime.