61

I recently completed a black-box refactoring. I am unable to check it in, because I can't work out how to test it.

At a high level, I have a class whose initialization involves grabbing values from some class B. If class B is "empty", it generates some sensible defaults. I extracted this part to a method that initializes class B to those same defaults.

I have yet to work out the purpose/context of either class, or how they would be used. So I can't initialize the object from an empty class B and check that it has the right values/does the right thing.

My best idea is to run the original code, hardcode in the results of public methods depending on the initialized members, and test the new code against that. I can't quite articulate why I feel vaguely uncomfortable with this idea.

Is there a better attack here?

2 Answers2

123

You are doing fine!

Creating automated regression tests is often the best thing you can do for making a component refactorable. It may be surprising, but such tests can often be written without the full understanding of what the component does internally, as long as you understand the input and output "interfaces" (in the general meaning of that word). We did this several times in the past for full-blown legacy applications, not just classes, and it often helped us to avoid breaking things we did not fully understand.

However, you should have enough test data and make sure you have a firm understanding what the software does from the viewpoint of a user of that component, otherwise you risk omitting important test cases.

It is IMHO a good idea to implement your automated tests before you start refactoring, not afterwards, so you can do the refactoring in small steps and verify each step. The refactoring itself should make the code more readable, so it helps you to increase your understanding of the internals bit by bit. So the order steps in this process is

  1. get understanding of the code "from outside",
  2. write regression tests,
  3. refactor, which leads to better understanding of the internals of the code
Doc Brown
  • 218,378
1

An important reasons for writing unit tests is that they document the component API somehow. Not understanding the purpose of the code under testing is really a problem here. The code coverage is another important goal, difficult to achieve without knowing which execution branches exist and how are they triggered.

However if it is possible to reset the state cleanly (or construct the new testing object every time), one may write a "trash in-trash out" type tests that just feed mostly random input to the system and observe the output.

Such tests are difficult to maintain, as when they do fail, it may be complex to say why and how serious it is. The coverage may be questionable. However they are still much better than nothing. When such test fails, the developer can revise the latest changes with more attention and hopefully spot the bug there.

h22
  • 965