Python Programming Language

Refactoring test units after an extract method

This is not strictly python related, but it's not strictly TDD related either. Anyway, here it goes.

There's something that I was never quite sure how to handle with test units: How to handle the test unit refactoring after a method extraction.

Let's say that you have a function foo() that does A and B. You have tests for foo() that check that A and B are executed properly. Then, you add another function, bar(), that needs to do B, so you add a third function, baz() that does B and make foo() and bar() call baz().

How to you handle the tests? Copy over the tests you had for foo() and apply them to bar()? I don't like copy and pasting code. Move the B related tests to baz()'s tests? Then your tests wouldn't fail if you stopped calling baz() in foo() and bar().

What I do right now is that I mock baz() and verify that it is called in foo() and bar(), with the right arguments. Then I move the B related tests to baz(). It kind of works, but somehow, it doesn't feel like the ideal solution to me. But then again, it's kind of the same thing as if baz() was a third party function: You have to mock it to test foo() and bar() properly without testing the third party code.

What do you people think?

--- Virgil Dupras <hardcoded.softw@gmail.com> wrote:

> How to you handle the tests? Copy over the tests you > had for foo() and > apply them to bar()? I don't like copy and pasting > code. Move the B > related tests to baz()'s tests? Then your tests > wouldn't fail if you > stopped calling baz() in foo() and bar().

I recently had an example where class A used class B, which used class C, which used class D, which used OS service E, etc. I had mock versions of B, C, D, and E. The overall design of the module was kind of like a network stack, where each module theoretically only depended on the services of the module beneath it in the stack.

So many of the tests for A used a real B, but a mock C.

Many of the tests for B used a real C, but a mock D.

Many of the tests for C used a real D, but a mock E.

But I also wanted to test that my abstractions weren't leaky, e.g. that there weren't some implementation details of C or lower that broke A. So certain loops in my test for A would also plug in a real C.

Finally, some of the test for A would also use a mock B.

I was able to mostly avoid copy-and-pasting of tests by taking advantage of Python's dynamic nature. Although this is oversimplifying things a bit, you can do things like this: