18

I was recently part of a TDD development team. At certain point I realized that there is a design mistake instead of Object Oriented Design and Structural design the problems were solved via statuses. Tens of statuses. For a brief moment I thought - Hmm... maybe I can actualy try to do some modeling and reduce the statuses. But I realize that the amount of generated Unit tests that relay on these statuses is so tremendous. At that point I lost all motivation to actualy perform the change.

If there is fundamental weakness in solution but the TDD cycle is already to far gone in the development. How do you do design change in TDD when you have such huge amounts of test that may be dependent on the existing implementation?

6
  • 3
    @DavorŽdralo: to be fair, I would say that's why TDD is only a good fit to the real world when it is done with a lot of care, by not throwing a lot of tests at a component without thinking about the consequences.
    – Doc Brown
    Feb 24 '21 at 16:02
  • 10
    @DocBrown - in my experience, TDD is only good if you go heavy on integration/API testing. I'm a primarily backend developer, and if tests are running against the API than it works pretty well. My main issue is that TDD has been evangelized by people who worship unit tests for no real reason. Feb 24 '21 at 16:05
  • Are you sure this problem is related to TDD itself? Tests are written to check whether your application is still working after such a refactoring, and fixing them would also mean fixing the parts of your application that needs to be changed according to that refactoring
    – Nico Haase
    Feb 24 '21 at 17:12
  • @DavorŽdralo: well, that is a pretty different statement than your first one ;-)
    – Doc Brown
    Feb 24 '21 at 17:16
  • 4
    @DocBrown - true, but in my experience TDD = unit tests. I wish it wasn't, but sadly it has always been the case. Feb 24 '21 at 17:27
24

How do you do design change in TDD when you have such huge amounts of test that may be dependent on the existing implementation?

First, the best approach may be to avoid running into this situation at first hand, and refactor earlier. Often people forget that refactoring should be done rigorously in each TDD cycle, and design flaws should be removed as soon as they become apparent. Given there are so many tests as you said, I am pretty sure the design flaw could have been spotted much earlier, at a point in time when there existed a lot fewer tests.

But what can you do if you have already painted yourself into that corner? The problem here is usually the amount of test code which uses the public-facing API of the Subject-Under-Test, which is what you want to change. So as an approach to resolve that problem,

  • try to reduce the number of direct test calls to the public API by refactoring the tests themselves first and make them more DRY.

  • build an anti-corruption layer, or a facade, or a proxy between your tests and the SUT, so you can change the API of the SUT without having to change too many parts of your tests. That will allow you to keep the tests as they are for now. Later, when you have some time for cleaning up, you may decide to migrate the tests to the new API one-by-one.

The latter approach is also known as strangler pattern and can often be used to gradually swap out legacy components by components with a new design, not only for tests.

As an example, if you have 50 tests calling the same public method of a class (and maybe only one or two places in your production code calling the same method), then the tests seem to hinder you changing the signature of that method. But if most of the direct calls inside the test code are rerouted through a single helper method, maybe one which also does parts of the arrange-act-assert work, it will become a lot simpler to change the method signature of the SUT.

2
  • As usual, your answer is much more elaborate so +1 :-) Feb 24 '21 at 5:54
  • @Hans-MartinMosner: thanks, I already voted yours up yesterday, before I wrote mine. "DDT instead of TDD" was a new idiom for me. I think our two answers have a different focus, both complementing each other.
    – Doc Brown
    Feb 24 '21 at 6:10
10

I saw the same problems when trying to do TDD. I think the solution is as simple as doing TDD when it makes sense and not dogmatically all the time.

When the requirements are clear, or when fixing a bug, it makes sense to write a test first. You will know when you're done, you can reproduce the problem, etc.

But when you design something that you'll likely need to iterate a few times, TDD is just an anchor. Also, there is a trade-off between having tests for everything and speed of change for the long term. If you have too few tests, you'll have no safety-net, but having too much tests cements everything in place. I think there's a balance somewhere between those extremes.

In summary: I saw the same things as you. If you already have those tests, I don't think you can do much (other then biting the bullet and doing the work of rewriting the tests too). Next time try a different balance between tests and ability to change.

3
  • I actualy decided to quit the project. I can not survive the feeling Of being helpless caused by this «cementing» you explained. Feb 24 '21 at 9:24
  • 1
    That's IMHO a really good answer (+1), I can confirm this observation, and I have no idea why it got a downvote.
    – Doc Brown
    Feb 24 '21 at 9:43
  • Definatly not from me :D I was also +1 Feb 24 '21 at 10:02
7

Fix the tests so they only test whether the requirements are fulfilled, not implementation details. If the status values are part of the specification (as is often the case with network protocols) then perhaps using them in the implementation is a good idea, but if not, it looks like you don't have TDD but DDT, design driven tests, which puts the cart in front of the horse.

How to communicate such a change is beyond the scope of this site, that's most likely a people problem.

5
  • Exactly! Thats the problem it is more of a DDT. But one side question if you don't mind. Something I don't get with TDD or maybe the project I was on is wrong about. Lets say we have Class A uses Class B uses Class C and B and C are just internal classes for A. Why do we need to test B and C realy ? Isnt this exactly testing the details when we should test the the public API ? Feb 23 '21 at 22:34
  • Your answer is acceptable , but will wait a bit if someone else will also answer, give chance to the others. If you can incorporate a bit about the DDT problem. Feb 23 '21 at 22:35
  • 1
    DocBrown goes into more details about what can be done. As a general idea, if you have internal classes that only support the implementation of the design, their tests should be subject to refactoring when you refactor the code. This might lead to throwing out a whole bunch of tests if you find that some class becomes obsolete in a changed design. Tests are a tool, not a goal. Feb 24 '21 at 5:53
  • 1
    @AlexanderPetrov Check out Ian Cooper's talk, TDD where did it all go wrong. I think you will find it interesting, he covers your first comment pretty well.
    – CaTs
    Feb 24 '21 at 7:12
  • 1
    And how do you "test whether the requirements are fulfilled, not implementation details" when most TDD evangelists heavily advocate for unit testing which turns every class interface into a requirement? Feb 24 '21 at 17:30

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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