7

Before I ask this question I will preface it with our environment. My team consists of 3 programmers working on multiple different projects. Due to this, our use of testing is mostly limited to very general black box testing.

Take the following assumptions also:

  • Unit Tests will eventually be written but I'm under strict orders to refactor first
  • Ignore common Test-Driven Development techniques when given this environment, my time is limited.
  • I understand that if this were done correctly, our team would actually save money in the long-term by building Unit-Tests before hand.

I'm about to refactor a fairly large portion of the code that is critical. While I believe my code will accurately work when done and after our black box testing, I realize that there will be new data that the new code might not be able to handle.

What I wanted to know is how to keep old code that functions 98% of the time so that we can call those subroutines in case the new code doesn't work properly. Right now I'm thinking of separating the old code in a separate class file and adding a variable to our config that will tell the program which code to use. Is there a better way to handle this?

NOTE: We do use revision control and we have archived builds so the client could always revert to a previous build, but I would like to see if there is a decent way of doing this besides reverting. I want this so they can use the other new functionality delivered in the new build.

Edit: While I agree I will need to write Unit Tests for this, I don't believe I will capture everything with them. I'm looking for ways to easily be able to revert to the old, functional code should anything happen. While I know this is a poor practice, I'm planning on removing this code after our team can guarantee that the new code works to the same standards as the old.

1
  • Is there a better way to handle this? No. If you can separate that part out of the code so neatly, then switch between the two so easily, this is the best way (if you can't do TDD - maybe even if you could).
    – MGOwen
    Commented Feb 9, 2011 at 4:50

7 Answers 7

8

You should write some tests for the existing code before you proceed with refactoring. As you refactor, keep running your tests to make sure that you haven't broken any expected functionality.

You may want to write unit tests for the code itself and also some integration tests for how other parts of your application interact with the part you're going to be refactoring.

There are frameworks you can use for this, or you can just write some interactive tests/small apps if you're pressed for time and your existing code isn't easily testable. You can keep the tests around for future reference, so that refactoring again in the future is easier to manage.


Without knowing how your code is structured and what language it's in, this is going to be a bit vague, but for easily swapping between the old version and new, you can employ polymorphism, dependency inversion, and dependency injection. I'm making assumptions here about a few things, including the fact that your code is object-oriented.

  • Extract the code that's about to be refactored into its own class or assembly.
  • Extract an interface out of it and keep that interface with the code that's calling your current implementation.
  • Change the calling code to use the interface instead of the concrete implementation. This will probably require modifying constructors to accept an instance of your soon-to-be-refactored class instead of creating one themselves.
  • Refactor your code. Make sure the new implementation still conforms to the same interface. Place your refactored code in a different class or assembly.
  • Use a configuration file or write some initialization code that will instantiate the right instance of your class (either new or old) and make it available to whatever code instantiates the code that uses your refactored class.

There are dependency injection frameworks (also known as "IoC containers") that might make this easier on you. If you search for one for your language, you'll probably find a few tutorials that'll make what I've said here more clear as well.

6
  • 2
    +1 because the time you spend writing those tests will pay off your technical debt and give you confidence in future refactorings
    – Gary
    Commented Feb 8, 2011 at 22:12
  • I agree there will need to be Unit Tests built for the new code but there are a lot of nuances to the algorithm and I don't believe I can capture all the bugs with Unit Testing.
    – jluzwick
    Commented Feb 8, 2011 at 22:16
  • 1
    @jluzwick Don't just build unit tests for the new code. Build them for the old code first and try to account for as many nuances as possible. Usually testing all possible paths through the code will give you reasonable coverage and confidence that your tests are representative of the algorithm they're testing. Then so long as the new code produces the same test results, you should be in a pretty good shape.
    – Adam Lear
    Commented Feb 8, 2011 at 22:21
  • @Anna Lear I'm planning on doing that to the best of my ability. But my time is limited on this Issue and many of the Unit Tests will have to be written after when there is time for them.
    – jluzwick
    Commented Feb 8, 2011 at 22:26
  • @jluzwick I've updated my answer. Hope that helps.
    – Adam Lear
    Commented Feb 8, 2011 at 22:35
3

Use unit tests to prove that the refactored code works as you expect it to. Don't keep the old code lying around in the codebase, otherwise you did all of that refactoring for nothing. As you said, you can always tell your current customers to refrain from upgrading if anything is critically wrong with the version of the application using the refactored code.

9
  • +1 - The whole point of refactoring is to get rid of old code.
    – Andrew
    Commented Feb 8, 2011 at 22:13
  • I completely agree with you, I recently joined this project and no Unit Tests currently exist. My manager isn't allowing me to spend that much time on Unit Tests unfortunately.
    – jluzwick
    Commented Feb 8, 2011 at 22:23
  • 1
    @jluzwick: Then I'm sorry to say that they don't understand what a proper software development process should include.
    – Bernard
    Commented Feb 8, 2011 at 22:46
  • @Bernard They don't, you could think of this organization as pre-CMMI Level 1.
    – jluzwick
    Commented Feb 8, 2011 at 22:48
  • 1
    @jluzwick, then your manager is an idiot. If you don't write tests to determine exactly, the current functionality, how can you guarantee the refactored system works as it should?
    – CaffGeek
    Commented Feb 8, 2011 at 22:54
3

I agree with the other posters that unit tests are very important. However, to try to answer your question, if you for some reason you cannot possibly write tests.

Ways other than tests to improve the correctness of your refactoring steps.

  1. Use automated refactoring tools. If your language/IDE has automated refactoring tools (e.g. eclipse, resharper) use them. They will allow you to extract interfaces and methods, move methods, rename things, create delegating adapters and many other things. Importantly they will always (or almost always ;-) do it right. And will detect situations where ambiguity could occur.
  2. Make your refactoring in steps which are as small as possible. Use very small refactoring changes that are highly likely to be successful. Follow all of the steps, don't skip them out or try to combine steps. The refactorings in Martin Fowler's book are very reliable if followed precisely.
  3. If you are using a language with good static typing; rely the type Type System and Compiler as much as possible. For example, if you have lots of parameters being passed around as strings and integers that could easily get mixed up, introduce a wrapper type for the parameters one at a time. This will help you understand the data flow in your application and make the code more self documenting.
  4. Use version control. Check in each small refactoring step. So if something unexpected happens you can go back through the version control history and re-evaluate what you did.
  5. Do not be afraid to revert. If you are refactoring and you get confused, find something unexpected, or find you need to do another refactoring first to allow the current one to continue - then revert your changes. When working in old complex legacy code I commonly revert many times before I find the a refactoring sequence that I am happy with. Use a pen an paper to keep track of refactoring opportunities you spot.
  6. Pair program. Two eyes are much better than one in this situation. You could even program as a triplet if you have the people available.
  7. Code review. After you have completed a sequence of refactorings, get someone else to read through the version control history and check that they agree with your reasoning and changes.
  8. Regular manual testing (as you say you can't have automated testing). The sooner after you make a change that the manual testing happens the less possible changes could have caused it, so the quicker you will be able to track down what has broken. Don't wait until you have made many changes then do all the manual testing at the end; as you will have little idea which of your changes has broken things.
1
  • I really like your point on 3. One part of the algorithm passes around 40 variables at one time. I've been trying to encapsulate these variables and separate them to make them more readable.
    – jluzwick
    Commented Feb 9, 2011 at 17:51
3

Refactoring without unit tests is really, really risky. If you have a very solid set of system tests, you might get away with it, but still, chances are, you will introduce new bugs. So one possible workaround is simply to write unit tests without telling your boss. If he is not micromanaging, he won't know what you are spending your time on, and you can just silently include the time to write unit tests into your estimates for refactoring tasks.

Regarding how to reimplement an existing codebase, see also this older answer of mine.

You added to your post:

While I agree I will need to write Unit Tests for this, I don't believe I will capture everything with them. I'm looking for ways to easily be able to revert to the old, functional code should anything happen. While I know this is a poor practice, I'm planning on removing this code after our team can guarantee that the new code works to the same standards as the old.

How can your team ever guarantee that? As you yourself correctly note, you won't realistically be able to capture all bugs with testing. Even more fundamentally, you can't prove by testing that a nontrivial piece of code is bug-free.

As I suggested in my earlier answer, IMHO the best you can achieve is to write a thorough set of tests against a common interface/facade, and develop the new implementation so that it satisfies all existing tests from the beginning.

Note also that there is a psychological barrier here: if you (and your users) don't trust the new implementation, you may keep deferring its deployment till the end of time. At some point, you have to make the leap of faith by declaring that it is good enough to replace the old one, releasing it officially, and retiring the old version, fixing any issues in the new implementation as they appear.

2
  • +1 -I was thinking the same thing, what does the boss care about the HOW, so long as it's built into the estimate and he is not in amongst the code micro-managing.
    – ozz
    Commented Feb 9, 2011 at 12:19
  • If I could give a correct answer twice, I would for this one.
    – jluzwick
    Commented Feb 11, 2011 at 17:39
0

So you've got an existing method, say, myMethod. You want to be able to continue to run as if it were still the old way - or to run it a new way. That's easy. Extract myMethod into myOldMethod. Write a replacement and call it, say, myNewMethod. Now myMethod looks exactly the same to the outside world, and it can call either myNewMethod or myOldMethod depending on whatever conditions you choose. The old code is still there, still usable, and you can flip a switch to compare old & new.

0

I empathize with your plight.

Your issue with not being able to write Unit tests, could potentially be addressed (provided your boss doesn't veto it) by writing more specific black box tests.

(You might even be using your time more efficiently in the long term with that approach ... compared to writing unit tests now. If you write unit tests for code you are about to refactor, the chances are that you'll need to overhaul the unit tests to cope with the refactoring.)

0

What I wanted to know is how to keep old code that functions 98% of the time so that we can call those subroutines in case the new code doesn't work properly

You can use the Sprout Method, as described in Working Effectively with Legacy Code. However, in order for the modified software to choose the suitable path in production, you need to implement a boolean predicate (function) for automatically choosing between the old or new behavior. If it is not possible for the program to choose automatically, then it will have to be configured manually.

If both the old and new software can reliably detect the cases for which it could not handle (either by doing a simple check, or by doing the actual work and then reporting the failure upstream), you can use the Chain-of-Responsibility pattern.

While I agree I will need to write Unit Tests for this, I don't believe I will capture everything with them.

Characterization Tests can be used to capture and characterize the existing behavior, so that there will not be unanticipated changes that break interoperability with other systems. If you have access to the old source code, you can write a tool to automatically generate a large number of characterization test cases and log the results. Since there is no need to exercise any human judgment in the correctness of results, it is fairly low-overhead to automate.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.