Friday, August 7, 2009

Ensure test coverage when refactoring to mocks

Using real dependencies in your tests will lead to pain. A lot of pain.

Most likely, your test fixtures lack coverage, test the wrong class, be difficult to read and don't do what they say.

Eventually, your team will decide to abandon testing or introduce mocking/stubbing.

Refactoring your existing tests will be a major endeavor.

It will be tedious and repetitive.

You will feel like you are wasting time.

You will have a strong urge to rush.

To help you out, I've outlined the steps needed to refactor a test to mocks. I've noted steps that are easy to skip (yet still important) with *

1. Make a copy of the TestFixture -- call it TestFixtureNameWithMocks -- this will ensure you do not lose test coverage and can commit often.

2. Comment out all tests

3. Delete the setup code

4. Take a moment to read the first test
  • Does this test actually test anything? (if not, figure out what it should be testing and get it working right in the original fixture first!)*
  • Does it verify state of the CUT?
  • Does it verify state of a dependent (or dependent of dependent of dependent...)?
  • Does it do more than one verification?
6. If there is more than one verification, determine how many tests you actually need.

7. If your test relied on the state of a dependent, verify that behavior is tested in the correct test fixture (i.e. the dependent's!). If not, you need one, use this test as an example. Create a failing test now, before you forget. If you have a rabbit hole of dependents, make a test for the top dependent.*

8. If needed, break out the test into multiple verifications -- select one to do, comment the rest.

9. Uncomment test and rewrite using mocks/stub. You will find that some tests are quite different. Some similar. Some will require refactoring of the CUT or dependents to make mock friendly.

10. Run the test and verify it passes. If it does not, figure out what preconditions are missing -- check the CUT and old test. Keep doing this till it passes.

10. Comment out code that is being tested.*

11. Run test, verify it fails for the right reason. If it does not, check the test and verify your assumptions. Make it fail for the right reason!*

12. Uncomment code, verify it passes. If you didn't change code in 11, you can skip this step -- I am paranoid and usually do it anyway :)

13. Repeat for every test in the fixture (and any new tests you find). Pull out common setup if needed. Rename tests since the original names will most likely need it :P

14. Remove old test fixture and rename new one.

If you made it this far imagine how long rewriting one test takes. In the best case you still need to run each test at least 2 times.

Now imagine doing this for more than one class.

How about for how many tests exist by the time our tests are painful enough to require this change.

If done right, this will take a lot of time. If we do it wrong, we take great risks and have tests that give us a false sense of security.

Clearly, if we want to continue delivering end user value, we cannot do this all at once.

Here is one way to do this:
1. Whenever a test is updated (bug fix, enhancement, refactor), update the test fixture to use mocks.
2. Have a policy where it is ok to have 2 fixtures for 1 CUT (one with mocks and one without) to encourage people to start using mocks w/o the overhead/context switch of a big refactor. Stress that finishing the refactor takes precedence over starting new work.
3. Let everyone on the team know that it is expected to take time and include that in commitments and estimations.
4. Remain disciplined

Good luck!!

7 comments:

Rob Park said...

This is great advice. If you have a coverage tool in place, that can also give you an additional bit of piece-of-mind. Your coverage should not go down unless you miss something by accident. But as usual coverage is not the master of truth, so following your steps would be the wise, disciplined thing to do.

Bill said...

Hi,

Perfect timing for this post for me as I have just been struggling through a situation like this trying to plan it out. Many thanks!

regards,
Bill

Anonymous said...

The matchless answer ;)

Anonymous said...

Useful piece

Anonymous said...

Genial post and this post helped me alot in my college assignement. Say thank you you on your information.

Markus said...

Loved the post, but I couldn't figure out what does CUT means? Can you enlighten the ignorant?

wunda said...

Markus, CUT means class under test