Sometimes I need to replace or refactor a function in a program. In this post I want to share a process I’ve used to make such change. The goal is to improve our confidence in the end result.
Suppose you have a functionf
we need to migrate. For the sake of simplicity, we will assumef
will never raise an exception.
deff(input:String)# ... stripped ...end
There are no unit tests, but said function is widely used through the whole suite of tests.
First step, leave the original function and define a wrapper.
deforiginal_f(input:String)# ... stripped ...enddeff(input:String)original_f(input)end
Second step, define a new implementation:
defnew_f(input:String)# something new we hope will workend
Third step, run both implementations and detect invalid behaviors ofnew_f
with respect tooriginal_f
. These invalid behaviors can be either crashes or unexpected results.
To detect them we are going to change the wrapper with a bit of code that you will probably not like at all. Relax, it will be gone at the end.
deff(input:String)original_result=original_f(input)beginnew_result=new_f(input)rescueee.inspect_with_backtrace(STDOUT)File.write("bug.txt",input)exit(1)endiforiginal_result!=new_resultFile.write("bug.txt",input)puts"\n\nUnexpected result:#{input.inspect}.\n Expected:#{original_result.inspect}\n Got:#{new_result.inspect}\n\n"exit(1)endoriginal_resultend
If there is an unexpected result the program will stop. Immediately. This can be done differently but, for the sake of simplicity, we are stopping at the first invalid behavior.
We run the whole suite of tests and, if there is a crash, abug.txt
file will be created with theinput
that caused the invalid behavior.
We can use a smaller program to work on the input that causes the crash.
deft(input)original_result=original_f(input)new_result=new_f(input)iforiginal_result!=new_resultpp!input,original_result,new_resultendend# Some trivial cases, maybet("")t("abc")t(" abc ")t(" ")# The input that caused the crasht(File.read("bug.txt"))# You could also use a test framework, of course.
This way we can work onnew_f
until the identified case is fixed.
Iterate the process of running the whole suite of tests until there is no crash.
Now you have anew_f
that works asoriginal_f
. At least,to the extent the suite of tests needs. Note that we are not talking about only unit tests off
.
Drop the "ugly" code to compare both implementations. Drop theoriginal_f
. We don't need them anymore. Leave only thenew_f
as the implementation off
.
deff(input:String)# something new we hope will workend
You are done! 🎉
This process was explained withCrystal but it can be adapted easily to other languages.
Programming is a tool. Yet, as a tool, it can be used as a process. We made temporal additions to our codebase as part of the process. There is no need for the code you write to always be permanent in the code base.
There are more advanced processes and tools in software verification. This is a small example that might motivate you to look into them.
Top comments(0)
For further actions, you may consider blocking this person and/orreporting abuse