DEV Community

Discussion on: Improve your life with TDD

Collapse
 
vdedodev profile image
Vincent Dedo

I've tried TDD many times and found it to be worse than TLD (Test Last Development). With TDD, don't you find that design suffers and it hinders your ability to refactor code beyond the internals of a function?

Collapse
 
mykezero profile image
Mykezero • Edited

I like this recent tweet from Ron Jeffries:

From his point of view, we must always keep design in mind at all times. This is regardless of using TDD or not using TDD. TDD might help with the design since the "refactoring phase" is really the thinking about design phase.

I've recently learned refactoring means not changing public API of a system: only the implementation should change. This might be what you meant by TDD only helping with changing the internals of a function.

fagnerbrack wrote a wonderful article on this exact subject:

Changing the API for something that is widely used through the system is hard, risky work. TDD helps us here, as a side benefit, of giving us a pretty comprehensive test suite that covers the entire system, since no functionality should be added without tests.

In these cases, one technique I use to improve the design of an existing system, is to write a new test using my "dream api". The API I wish I had.


We start with our existing API.

Sum(int x, int y) => x + y;

I might think my design is bad, and I'll start by playing around with the design in a new test:

Maybe this might handle future inputs better?

testSumDesign => Sum(Inputs: {1,2}) == 3

If I like this design, I now run into a new problem: this API is used in 3000 parts of the system.

To solve this problem, we'll incrementally switch existing consumers over to the new function.

Start by giving the new Sum function a different name: Sum2?

Sum(int x, int y) => x + y;
Sum2(Inputs) => Inputs.X + Inputs.Y

Now, in the existing tests, update all occurrences of Sum to use Sum2.

testSum => Sum(1,2) == 3

should be changed to

testSum => Sum2(Inputs: {1, 2}) == 3

The test should pass and we know Sum2 is compatible with every behavior that Sum implemented.

Now, you can begin the process of slowly transitioning every consumer of Sum to use Sum2:

AddThreeNumbers(1,2,3) => Sum(Sum(1,2), 3)

should be changed to

AddThreeNumbers(1,2,3) => Sum2(Inputs: {Sum2(Inputs: {1,2}}, 3)

After transitioning a few of them, I should pause and think about whether my new design is better or worse than the old one (In this case, I've actually made it worse: back to the drawing board...)

It may not be that TDD makes design suffer, but redesigning the API of heavily used components is painful work.

Hopefully that helps a little bit! ;

Collapse
 
franiglesias profile image
Fran Iglesias • Edited

Thanks for your comment :-)

In fact, my experience is the opposite. By doing TDD I feel myself less tied to a particular implementation, so I can start with a rough and naive approach and improve the design iteratively being well protected with the tests at every moment. It is far easier for me to explore ideas, extract methods or classes and, in general, develop a better design and code less prone to defects.

When I write code without TDD I tend to "fall in love" with my current approach and when I return to this code after some time I detect more design flaws and defects.

Collapse
 
vdedodev profile image
Vincent Dedo

How do you do design with TDD? That's one of the things that doesn't make sense to me.

Thread Thread
 
franiglesias profile image
Fran Iglesias

For example, imagine I need to write a new use case for an application.

I first start TDD'ing the use case as if it would be the unique class I'm gonna need.

Usually I start testing expecting exceptions because they usually are the simplest, but this may vary. I look for the simplest behavior. Sometimes this is very straight, some times it is a bit difficult to find it.

With every new test I may discover that I need collaborators for my use case in order to get data or services. Maybe it is a repository that I will need to double for testing, maybe it is some kind of service that already exists in the code, so I usually double it also. Sometimes I prefer use it as is.

Test by test I write the needed code to resolve my problem, and when I'm in the refactor phase I extract parts to private methods in the use case, that can lead me to extract some behavior to a new collaborator. This new services and collaborators are covered by the initial use case tests, so I don't need to separately test or add them (the collaborate with the Use Case to perform the behavior). I will add tests if I want to be more confident about them.

Of course, depending on the outcome of the UseCase (Is it a query returning something or is it a command?) I will need to test for a response or for a side effect, maybe I need to use a mock to verify some behavior.

Hope that helps.

Thread Thread
 
franiglesias profile image
Fran Iglesias • Edited

By the way, this post by Uncle Bob is very interesting to understand this approach:

blog.cleancoder.com/uncle-bob/2017...

Thread Thread
 
vdedodev profile image
Vincent Dedo

That's completely new to me in terms of TDD, but then again it doesn't sound like TDD and more decoupling. What about unit tests?

Thread Thread
 
franiglesias profile image
Fran Iglesias

I'm not sure if I'm explaining well the methodology, but maybe you can see it in action in this example (text is in Spanish, but I think you can follow the code samples to get the overall idea)

leanpub.com/testingytddparaphp/rea...