8 ms·
TDD Doesn't Force Good Design
- ChatGTP 3y agoI think the simple claim that TDD improves design isn't strictly true, and turns some people away from automated testing. Never really heard this claim before ?
- beardedwizard 3y agoCame to say this, never heard this before. I have heard the claim that it improves quality, but even that is debatable. Im sure the most basic claim that can be proven is it often increases test coverage.
- js8 3y agoImprovement in quality is improvement in design, since having less bugs is one of the desired properties of good design.
- bayindirh 3y agoA bad design may work equally well when compared to a good design, given it's patched enough.
- wizofaus 3y agoUnless the bug fixes are so fragile and/or convoluted they make any change in business requirements almost impossible to implement.
- beardedwizard 3y agoTotally disagree. A bad design can pass all tests and appear high quality, then crumble under high load. Even quality is subjective. Most people would assume it means being good at every aspect, but passing tests doesn't not guarantee that.
- gumballindie 3y agoIt's a common fallacy among some software developers that since TDD forces you to write small units of code (since you have to write small unit tests then naturally the units you test will also be small) then your code will be easy to maintain. It also forces dependency injection in OOP and a couple other design patterns. I've seen code written as if by one's feet without unit tests that worked better than code with 90% test coverage. In a few cases tdd was the go to practice for sloppy devs. Somehow they thought that writing tests will account for lack of skill. But guess what? Tests were sloppy too.
- wizofaus 3y agoI don't think anybody could claim TDD will make a sloppy dev write good code. Though there's half an argument for getting more experienced/skilled devs to write the tests and the more junior or less capable ones to fill in the actual working implementation (but I'll be honest, I've never seen this work in practice over a prolonged period).
- gumballindie 3y ago> (but I'll be honest, I've never seen this work in practice over a prolonged period) Me neither. I think that can only work when the same thing has been done countless times before. But at that point you can just use someone else's work, either via a library or an off the shelf product, instead of reinventing the wheel. And then you are left with working only on something that's new. Which invites the question - how can you test that which you don't know you need? In the context of unit testing that is.
- sbergot 3y agoIt is a common claim made by TDD proponent. The reasoning is that it gives you a better perspective of the external facing apis you are developing because you are forced to use them when writing the tests.
- deleted 3y ago[deleted]
- rewmie 3y ago> Never really heard this claim before ? Neither did I. I guess it really depends on what the code would be like without having the constraints to make it testable. A big ball of mud component hardly qualifies as good design, and TDD definitely dissuades people from that mistake.
- ryandv 3y agoIf tests are written first, (_and_ the dev has a good eye for design, which is not always given) then it is likelier that you will write a more ergonomic interface (e.g. methods having smaller number of parameters/dependencies, "intention revealing names", [0] etc..). In a unit-testing paradigm, this also means that you will need to inject the unit's dependencies so that it can be driven into specific states by each test in the suite, encouraging dependency inversion and the open/closed principle (as given by Uncle Bob [1]; note that with dependency inversion it becomes possible to vary the behaviour of the unit in test vs. production environments simply by specifying different dependencies without modifying the unit's code itself). If the dependencies are too cumbersome or even impossible to set up, then the test reveals something about the design (e.g. high coupling). Of course, it's also possible to just grin and bear the pain of tests being too difficult to write, without actually resolving the design problems revealed by the test... [0] https://wiki.c2.com/?IntentionRevealingNames https://wiki.c2.com/?IntentionRevealingNames [1] https://web.archive.org/web/20060822033314/http://www.objectmentor.com/resources/articles/ocp.pdf https://web.archive.org/web/20060822033314/http://www.object...
- charlysisto 3y agoI heard this a few times in the ruby community by some well known figures like Sandy Metz. Simple example : time related tests can be cumbersome to setup, but if you inject time in the method, test becomes easy, incidentally improving your design by not relying on globale objects... But I am sceptical this can work without knowing and practicing good design principles (SOLID) independently from tests ! However I believe tests are super important for refactoring, wether the design is good or not. My 2 cents : if the design is bad, (spaghetti and all) tests should be done more at the functional level, when your confident design is getting good then rely more on unit test
- chii 3y agoThere's no mechanical process from which blind adherence could produce good design without the author/engineer having experience or knowledge (or both). It just stops bad design from setting in too easily - and even then, it's "easy" to workaround or apply the process incorrectly or too pedantically.
- AnimalMuppet 3y agoAs you say, TDD can make it "less easy" for a bad design to settle in - if you pay attention. If it's hard to write the test, it may be telling you something about the design. If you listen, that can push you toward fixing the design. If you don't listen, well, you're just going to write the tests that you're told to write, and probably grumble about it, but that doesn't help the design at all.
- chii 3y ago> If you listen requires that you know what to listen for. I've seen junior developers write terrible tests. The thing is, to know what a good design looks like require experience. To know that the test was hard or bad requires experience, or at least knowledge of what makes a test good. If you've never seen a good test, you might think the test you wrote was good, and the pain is just normal.
- sprucebruce 3y agoThis is exactly what I'm getting at in the blog post. I think the message that it works "if you listen" needs to be front and center so that learners and skeptics can figure out how to apply TDD in scenarios where they don't have any hands on guidance.
- dopidopHN 3y agoCould someone elaborate on why “mocking is a smell”? Is it opposed to running the whole system with real interactions? I find it a nice shortcut, but true as well than the mocking behavior can be non existent in the real code path. Still, I reach out to mocking libraries often. Is that a bad practices and why ?
- deleted 3y ago[deleted]
- frankdejonge 3y agoMocking, when it is a large part of your test setup, makes your test focus on the blueprint of the implementation rather than the outcome of a unit of behaviour. This causes the tests to break relatively soon when the implementation changes. When a test does not use mocking and rather treats the code as a sealed box, the tests are more stable. Stable tests yield better guarantees for stable outcomes even when the underlying code is (radically) changed/refactored. Using mocking frameworks usually makes refactoring harder, as changing the underlying code immediately invalidates the test. Changing the tests and code in unison makes changes less safe.
- frankdejonge 3y agoI must add that it's mostly mocking libraries that auto-generate mocks that are the culprit here. They're quick to introduce, but once introduced make it harder to change the implementation as the implementation logic is now replicated in N instances of tests. More surface area, more friction for change. Many mocking libraries also allow you to fixate behaviour that is incompatible with the actual implementation, especially in dynamically types languages.
- geewee 3y agoI think it depends heavily on what you're mocking and what the code you're testing looks like. I think Martin Fowler has a nice dichotomy where he splits tests up into solitary (generally using mocks for deps) and sociable (generally using real code for dependencies): https://martinfowler.com/bliki/UnitTest.html https://martinfowler.com/bliki/UnitTest.html
- frankdejonge 3y agoMost solutions on their own don't improve things a whole lot. Yet, in a system of supporting practices, it can be very powerful. The primary thing is you need a system, not just the individual parts. Testing without changing the design of your code is a horrible experience. Applying techniques like dependency inversion/injection has a positive effect on isolating behaviour which makes testing easier. Making code more deterministic makes tests easier. Pushing out side-effect from your core logic makes testing easier. All of those things add up to more than the sum of it's part, which is the an indication of dealing with a system.
- MoreQARespect 3y agoDependency injection is only useful when you've managed to isolate logic/math intensive code. Some apps dont have any logic intensive code. Many others have very little. In those cases dependency injection just increases the SLOC with the payoff that you are able a bunch of trivial unit tests that'll probably never catch a bug. Integration tests as a default have a the best ROI in those cases.
- taberiand 3y agoThis isn't really true. For one, dependency injection isn't specific to testing or processing logic, it's an architectural approach used primarily for managing separation of concerns and modularisation.
- MoreQARespect 3y agoI didnt say it was specific to testing. Nor did I say it was never appropriate. Nonetheless the pursuit of "unit testability" is probably most often why it's done.
- tonyedgecombe 3y ago>it's an architectural approach used primarily for managing separation of concerns and modularisation That always feels like a post hoc justification to me. If you demand 100% test coverage on a Java/C# project then you will almost certainly end up with some kind of dependency injection at the end. Whether that is useful or not is often debatable.
- Dudester230602 3y agoIt's not TDD that I don't like it's the zealously of its proponents. In any case, TDD helped trainers put their kids through college, now the hype is over and we can be productive again. PS: thanks god, mainstream pair programming is dead too.
- tonyedgecombe 3y agoTo be fair there are plenty of anti TDD zealots as well.
- donatj 3y agoI have absolutely seen high test coverage crystallize bad design. You are less likely to rewrite something tested and proven to work. I have also seen high coverage that didn’t actually test much of anything. Code with side effects, none of which tested, just more or less that the method ran without a runtime exception. Coverage is a good tool but a bad metric.
- Cthulhu_ 3y agoIn my experience, test code has much more volume than the implementation in a lot of cases, but clean coding practices and maintainability go out the window; I often can't make heads or tails out of a unit test hundreds of lines long, so I become hesitant to change the component or its tests. Which defeats the purpose.
- capableweb 3y agoPeople have this aversion of treating code for tests as production code. They spend time designing the architecture for whatever they are implementing but when it comes to writing tests, all of that stuff goes out the window and people just do whatever takes the least amount of time. If you start treating tests as any other code, it should get easier to change as your implementation changes, and you'll get a lot more value out of automated testing.
- tetha 3y agoI've been challenging myself in a current work project. I don't want duplication in tests and I want most methods to have zero setup. The high level tests ideally should just start with a get-request and go straight into expectations. For one, this has resulted in a curious structure. I have a 5 or 6 fixtures with representative sample datasets used in production and these get extended as new features are introduced. If some new addition breaks everything, we'll know immediately, as all tests use the same fixtures and it's all just one place. And it's also instructive during design. These fixtures give you 3-4 situations to think through when adding something new. And the tests are pretty. Most of them have been boiled down into one test_client request and a comparison with a dataset, just 2-3 logical lines. This makes is easy to see if tests are still correct, necessary. Or to just talk about if they make sense overall, even with less technical or involved people.
- m000 3y agoFairly obvious, but if you do dumb things and expect a general methodology to save you, it won't. Or, like it is said in fitness world, "you cannot out-train a bad diet". Moreover, if you have people in your team that are not aligned with the methodology ("I'd rather be doing X", "It slows me down as a developer" etc.) the results will be sub-par. Applying a methodology requires a certain level of mindfulness which doesn't seem to be common these days.
- BackBlast 3y ago"Hey, we aren't seeing results from TDD." "It must be the bad attitudes on the team." Or... TDD just isn't the panacea it's proponents make it out to be?
- troupo 3y agoTo quote Kent Beck of Extreme Programming fame [1]: --- start quote --- I get paid for code that works, not for tests, so my philosophy is to test as little as possible to reach a given level of confidence --- end quote --- In my opinion, TDD as proposed by testing zealots is harmful. Instead of testing a unit of work testing frameworks and methodologies all but force you to test units of code, and that is not the same thing. E.g., if your code responds with a list of items from a database, you should test that. Instead, people test the controller in isolation (mocking everything), the transforming function in isolation (mocking everything), the database layer in isolation (mocking everything)... No. You have to test that your code, when called, returns a list of items. If you can't have the database, mock that, and only that. Boom, you have 1500% fewer tests, with the same coverage, and an actually increased level of confidence. [1] https://stackoverflow.com/a/153565 https://stackoverflow.com/a/153565
- tetha 3y agoYeah, I've found I'm starting to write fewer, and coarser tests and it is good for the code base. You can always augment them with bugs later on. And if you can isolate the main logic of a system into small components extended by addition over modification, even better. In most of the small systems we're developing at work, I tend to have 2 groups of tests: One testcontainer driven test testing the actual database layer. These tests are very simple and small - migrate the database, call insert a few times, load objects back and compare them. Nothing more. And the other tests are usually more of the nature of "Given these elements in the database, expect that output for a REST call", augmented with a few tests of some fiddly functions. The other thing I meant occured for example in a stream processor, which needed to apply different transformations to incoming messages for normalization, default values, precomputations and so on. Here we reached a point of confidence where each of these processors was tested as a stream to stream mapping in isolation. It was pretty and it worked wonderfully and without issues. If you can achieve this kind of system of pretty much functional components and extension by addition, that's very much a pinnacle of testability.
- Supermancho 3y ago> You have to test that your code, when called, returns a list of items. What do you do when the list it wrong? (eg, not transformed/serialized/sanitized/whatever) Sometimes, manual tests show it's not the expected/correct data from the first run; or later someone adds a line of code (adds a period to a random row). The test runs happy, even thought the data is now incorrect/unexpected. Moving something that can be automated with mocks, to manual processes, is a bad tradeoff.
- mrjin 3y agoI don't really know where such claim came from. TDD is really about getting features tested. I never heard similar claims before.
- rgoulter 3y agoReusable modules of code involve implementation and the interface. If you just write out an implementation without thought for how others are to use it, there's a risk that you'll end up with an interface that's more complex to use than it needs to be. The idea is that by 'using' the interface of the module first (e.g. by writing tests first, or by writing documentation first), then the design of the interface isn't left to be an afterthought of the implementation. Similarly, it's easier to consider edge cases when thinking about unit tests / documentation, etc.
- meheleventyone 3y agoThat’s amazing because people have been arguing about TDDs impact or lack of as a design technique for over a decade!
- voiceofunreason 3y ago"I never said that test-first was a testing technique. In fact, if I remember correctly, I explicitly stated that it wasn’t." -- Kent Beck, September 2001. Similar claims have been part of the TDD tradition since its very beginning.
- yungporko 3y agonothing reliably forces good design, or good code. a shitty team will design a shitty application full of shitty code regardless of testing methodologies or anything else. what else is new? does that mean that literally all development processes are bad and should be dismissed and everybody should just open their editor and start writing, organisation be damned? seems that way if you read comments on literally any post on hackernews.
- capableweb 3y agoI agree that there is nothing that forces good design, but if you force people to take more time to think about the design, it increases the likelihood of good design. Problem is that people don't either take the time to think about it, or they're not given enough time.
- deleted 3y ago[deleted]
- lambda_dn 3y agoI've seen code bases where a lot of the tests are actually testing the underlying framework's code (i.e. dotnet framework) after they mocked out all the dependent services. In my mind unit tests are a waste of time except for testing really complex algorithms where the there are no side effects. Automated end to end QA testing is where the effort should be spent.
- michaelsbradley 3y agoUnit tests are mostly for hitting branching logic within your prodedure/method/function (and private ones in your codebase that it calls but can't be tested independently, e.g. they're private), and so are naturally paired with code coverage. If your percent coverage is high and someone changes the implementation, a drop in coverage and/or failure of one or more unit tests can signal that some assumptions about the behavior may need additional consideration. End-to-end testing is important too, but it may be difficult to ferret out tricky bugs if unit tests and coverage for your modules (or whatever your lang/kit calls them) don't tell their own story about what code is being exercised and how.
- rob74 3y ago> If testing your code is hard it means your code needs to be factored better. Stop what you’re doing and fix the design so that testing is easy again. Then again, another central selling point of having tests is that it enables refactoring without having to fear that you change the behaviour of the code. So, if you have a codebase without tests, you should write tests while changing the code as little as possible, and then refactor it.
- krm01 3y agoThere's a parallel in the design world that remains under-discussed. In my journey as a small studio owner, collaborating with numerous startups, I've found that raw experimentation, while valuable, isn't the only path to innovation. A startup can conduct endless user interviews, much like a developer can persist with TDD, but at some point, the sheer weight of expertise begins to eclipse the utility of trial and error. There's a pattern to human behavior, just as there is in company challenges; a sort of predictability within the myriad of unique use-cases. So, as much as startups are evangelized about "talking to users", there's an underestimated power in simply hiring seasoned talent. I for example have a decade+ worth of experience designing B2B SaaS products. Many challenges have familiar contours and are solvable without the rigmarole of repeated experimentation. Just as TDD can be misinterpreted as a silver bullet for flawless code, so can raw user feedback for impeccable design. The nuance, often, lies in the marriage of systematic process and accumulated wisdom.
- sshine 3y agoHistorically, those of my (non-delusional) colleagues who made poor UX/API design don't defend it being good. They say things just had to get done. Sometimes management incentives tilt towards making poor solutions (deadlines used to justify a hasty job), other times developers just don't care.
- tonyedgecombe 3y ago>other times developers just don't care I've seen that far more often than management pressuring people into writing bad code.
- deleted 3y ago[deleted]
- d--b 3y agoTDD is a tradeoff. TDD improves good design *for the user* at the expense of implementation design *for the coder*. Because you write how the code is going to be used first, you're completely free of implementation constraints, so you can make it as easy as possible for the user to use your library/app/whatever. This makes it harder for you because now you have implementation constraints that you didn't necessarily have if you started without TDD. That's the whole point of TDD.
- ryandv 3y ago> I experienced this first hand when I first started learning to do TDD with unit tests. We had a badly designed system and we adopted a unit testing strategy that said every class needed to be tested independently from every other class. Testing this system was hard. It was a lot of effort with mocks and dependency wrangling to exercise our classes, and it produced brittle tests that never added any value in terms of the quality of the overall system. > We were writing bad tests in a badly designed system and having a horrible time. The conclusion we came to was that this testing thing was for the birds. It was slowing us down and not really adding any benefit. Get rid of it. If the tests are revealing that the system is "badly designed," the solution is not to throw away the tests in my view - it's to work on gradually refactoring the system to address the difficulties with testing it (of course subject to developer time/budget constraints...). The tests provide a gauge of the "internal quality" of the system (to take a concept introduced by the text "Growing Object-Oriented Software, Guided by Tests" [0]). xUnit Patterns has published a list of "test smells" and possible defects they may indicate in the design of the system: http://xunitpatterns.com/Test%20Smells.html http://xunitpatterns.com/Test%20Smells.html To be fair the author does call this point out in their conclusion: > If testing your code is hard it means your code needs to be factored better. Stop what you’re doing and fix the design so that testing is easy again. I would also add that adhering to the testing pyramid [1] principle is important, such that your system is not overly reliant on mocks (which can diverge from the actual "production" behaviour of the system) and/or brittle unit tests that break easily when changes to the system are introduced. [0] https://www.google.ca/books/edition/Growing_Object_Oriented_Software_Guided/QJA3dM8Uix0C?hl=en&gbpv=1&dq=growing+object+oriented+software+guided+by+tests+%22internal+quality%22&pg=PT34&printsec=frontcover https://www.google.ca/books/edition/Growing_Object_Oriented_... [1] https://martinfowler.com/articles/practical-test-pyramid.html https://martinfowler.com/articles/practical-test-pyramid.htm...
- drewcoo 3y agoHuzzah! I'll add that the meme of preferring integration tests to any other tests seems to stem from badly designed code. Looking at production code less closely disguises many sins.
- 3y ago
- knallfrosch 3y agoI feel like unit tests slow down a framework-heavy application, like an Angular app. The more you refactor and split, the more your unit tests will look like FakeAsync Fakeclick MockService was called with FakeParameters Doesn't really add much value over click triggers function calls service
- jacknews 3y ago"The claim that automated testing and TDD forces you to produce better designed systems isn’t strictly true" Has TDD become so sacred that we need to beat around the bush? TDD quite often in fact forces bad design, and combined with agile and especially scrum, often 'forces' myopic architecture too. There, I said it. Some kind of specification and testing is still way better than none at all, but current practices are not without potentially deep problems.
- Fire-Dragon-DoL 3y agoAgree. I saw a codebase with extreme tdd and the result is that it could achieve an extreme amount of mocking (and it did). The design was bad, but very testable.
- imbnwa 3y ago> Especially in languages like javascript you can do a lot with mocking frameworks and dependency orchestration hacking to make bad code work. This is precisely why I do everything I can to block usage of Jest, and more generally, another reason we need to sunset CommonJS. People just abuse this stuff. There is a fine line in our craft where your tools need to hinder convenience, hence why we have much discussion around strongly-typed vs weakly-typed, why TypeScript has gained so much traction, etc. Vast majority of 'business' stakeholders are going to exert downward pressure for the most convenient option, but the consequence isn't merely this or that quality of code design, but that subsequent generations of developer _have no chance_ of developing actual system design skills, evaluation of patterns, etc. And why should they? Use Jest to mock all the things, slap the 100% code coverage label on it, and start moving the next ticket to 'Done'.
- nunez 3y ago> We had a badly designed system and we adopted a unit testing strategy that said every class needed to be tested independently from every other class. Testing this system was hard. It was a lot of effort with mocks and dependency wrangling to exercise our classes, and it produced brittle tests that never added any value in terms of the quality of the overall system. Sounds like the codebase is the problem, not the tests. Too much tight coupling.
- pornel 3y agoThere's a pattern in technical blogging: take a guideline, interpret it as a guarantee or an absolute requirement, and then show it doesn't always work. Then you can have a catchy title, sounding almost like a proof by contradiction invalidating the whole guideline. You can make a mess out of any methodology, design pattern, or language. There are always exceptions and trade-offs. You can find cases where tests don't work, types don't work, DRY doesn't work, YAGNI doesn't work, etc. but it shouldn't lead to conclusion that copy-pasting Perl is just as good as any other approach.
- ehutch79 3y agoKeep in mind that very often, the person interpreting as a hard requirement, and the on showing it's not, are different people. There's a lot of cargo culting going on, especially on twitter. This has only gotten worse since twitter started paying out on engagement.
- godzillabrennus 3y agoControversy brings clicks. Attacking methodologies is easy click bait.
- sprucebruce 3y agoHowdy - this blog post is responding to folks who do claim that TDD makes it "literally impossible" to design bad systems. This is a fairly common claim among TDD advocates, and I think it makes it harder for learners and skeptics to understand the methodology. I'm not attacking TDD, I'm a big fan of TDD, I'm just attacking the claim that TDD forces good system design. I understand in retrospect why the title of the piece reads as click-baity, but that wasn't intentional. I only just noticed this morning that this got shared over here to HN, so now I'm gonna poke around here and find out what horrible things yall have been saying about me ;P
- tarcon 3y agoInside-Out TDD doesn't necessarily drive your architectural design, that's right. But Outside-In TDD can do so.
- taylodl 3y agoTDD ensures your system/application is testable, that's it, and that's all. Seriously. TDD does not, nor ever has, said anything about the quality of the design. It merely says it will be testable. While a design that is testable may not be "the best" design (presuming some agreed-upon ordering of goodness of design, which there isn't), it is at least testable and can be proven to work as intended. That, in the end is what this is all about. Do you go with what people feel is the best design but you can't prove the result, or do you go with what people feel is the poorer design but you can test the result and prove it works? Make this decision as though your life and/or life savings depended on it.
- tmaly 3y agoI agree with your statement. I think where TDD may not work well is with legacy systems where people try to bolt on tests after the fact. I get the impression from the article that this might be the case.
- taylodl 3y agoIf legacy systems weren't designed to be tested then it can very well be that they're very difficult to test. I've never seen anyone try to apply TDD to projects modifying those systems. Seems like it'd be an exercise in frustration!
- sprucebruce 3y agoThere's a great book by Michael Feathers on this very subject called "Working Effectively with Legacy Code". He takes a strong "test first" approach and talks about all the work you have to do when working on a system that isn't testable to change it safely with TDD.
- taylodl 3y agoThanks for the info! I'll check it out.
- switchbak 3y ago
- NikkiA 3y agoPure TDD as originally described 'write the minimal function to satisfy the test' does not actually encourage any design at all, just throwing code together on the fly.
- tincholio 3y agoDefinitely, this...
- MathMonkeyMan 3y agoThen it's about designing the tests. Maybe that _is_ a good idea...