6 ms·
I'm fully aware of the idea that TDD is a "design practice" but I find it to be completely wrongheaded. The principle that tests that couple to low level code
by MoreQARespect 3y ago
I'm fully aware of the idea that TDD is a "design practice" but I find it to be completely wrongheaded.
The principle that tests that couple to low level code give you feedback about tightly coupled code is true but it does that because low level/unit tests couple too tightly to your code - I.e. because they too are bad code!
Have you ever refactored working code into working code and had a slew of tests fail anyway? That's the child of test driven design.
High level/integration TDD doesnt give "feedback" on your design it just tells you if your code matches the spec. This is actually more useful. It then lets you refactor bad code with a safety harness and give failures that actually mean failure and not "changed code".
I keep wishing for the idea of test driven design to die. Writing tests which break on working code is inordinately uneconomic way to detect design issues as compared to developing an eye for it and fixing it under a test harness with no opinion on your design.
So, yes this - high level test driven development - is TDD and moreover it's got a better cost/benefit trade off than test driven design.
- surgical_fire 3y agoI don't even like TDD much, but I think that this missed the point: > Have you ever refactored working code into working code and had a slew of tests fail anyway? Yes - and that is intended. The "refactor of working code into working code" often changes some assumptions that were made during implementation. Those tests are not there to give "feedback on your design", they are there to endure that the implementation does what you thought it should do when you wrote your code. Yes, that means that when you refactor your code, quite a few tests will have to be changed to match the new code. But the amount of times I had this happen and it highlighted issues on the refactor is definitely not negligible. The cost of not having these tests (which would translate into bugs) would certainly have surpassed the costs of keeping those tests around.
- gavmor 3y ago"Have you ever reconsidered your path up the cliff face and had to reposition a slew of pitons? This means your pitons are too tightly coupled to the route!"
- thom 3y agoIf we’re talking “what you thought it should do” and not “how you thought it should do it” this is all fine. If requirements change tests should change. I think the objection is more to changing implementation details and having to rewrite twice as much code, when your functional tests (which test things that actually make you money) never changed.
- chris_wot 3y agoIf your functional tests fail because you made an “unrelated” code change, then you’ve done something wrong.
- naasking 3y agoMaybe, but I think the point is that it's probably very easy to get into this situation, and not many people talk about it or point out how to avoid it.
- chris_wot 3y agoI’m still not following what the issue is. If you refactor some code and change the behaviour of the code, and the code tests the expected behaviour and passes, then you have one of two problems: 1. You had a bug you didn’t know about and your test was invalid (in which case the test is useless! Fix the issue then you fix the test…) or 2. You had no bug and you just introduced a new one, in which case the test has done its job and alerted you to the problem so you can fix your mistake. What is the exact problem? Now if this is an issue with changing the behaviour of the system, that’s not a refactor. In that case, your tests are testing old behaviour, and yes, they are going to have to be changed.
- _gabe_ 3y agoWhy does testing behavior matter? I don’t care if my tests exhaustively test each if branch of the code to make sure that they call the correct function when entering that if branch. That’s inane. I care about whether the code is correct. A more concrete example; say I’m testing a float to string function, I don’t care how it converts the floating point binary value 1.23 into the string representation of “1.23”. All I care about, is the fact that it correctly turns that binary value into the correct string. I also care about the edge cases. Does 0.1E-20 correctly use scientific notation? What about rounding behavior? Is this converter intended to represent binary numbers in a perfect precision or is precision loss ok? If your tests simply check that you call the log function and the power function x times, your tests are crap. And this is what I believe the parent commenter was talking about. All too often, tests are written to fulfill arbitrary code coverage requirements or to obsequiously adhere to a paradigm like TDD. These are bad tests, because they’ll break when you refactor code. One last example, I recently wrote a code syntax highlighter. I had dozens of test cases that essentially tested the system end to end and made sure if I parsed a code block, I ended up with a tree of styles that looked a certain way. I recently had to refactor it to accommodate some new rules, and it was painless and easy. I could try stuff out, run my tests, and very quickly validate that my changes did not break prior correct behavior. This is probably the best value of testing that I’ve ever received so far in my coding career.
- thom 3y agoI think many people realise this, thus the spike and stabilise pattern. But yes, integration and functional tests are both higher value in and of themselves, and lower risk in terms of rework, so ought to be a priority. For pieces of logic with many edge cases and iterations, mix in some targeted property-based testing and you’re usually in a good place.
- deleted 3y ago[deleted]
- aljarry 3y ago> Have you ever refactored working code into working code and had a slew of tests fail anyway? That's the child of test driven design. I had this problem, when either testing too much implementation, or relying too much on implementation to write tests. If, on the other hand, I test only the required assumptions, I'd get lower line/branch coverage, but my tests wouldn't break while changing implementation. My take on this - TDD works well when you fully control the model, and when you don't test for implementation, but the minimal required assumptions.
- lukeramsden 3y agoPart of test-driven design is using the tests to drive out a sensible and easy to use interface for the system under test, and to make it testable from the get-go (not too much non-determinism, threading issues, whatever it is). It's well known that you should likely _delete these tests_ once you've written higher level ones that are more testing behaviour than implementation! But the best and quickest way to get to having high quality _behaviour_ tests is to start by using "implementation tests" to make sure you have an easily testable system, and then go from there.
- Dylan16807 3y ago> It's well known that you should likely _delete these tests_ once you've written higher level ones that are more testing behaviour than implementation! Is it? I don't think I've ever seen that mentioned.
- MoreQARespect 3y ago>It's well known that you should likely _delete these tests_ once you've written higher level ones that are more testing behaviour than implementation! Building tests only to throw them away is the design equivalent of burning stacks of $10 notes to stay warm. As a process it works. It's just 2x easier to write behavioral tests first and thrash out a good design later under its harness. It mystifies me that doubling the SLOC of your code by adding low level tests only to trash them later became seen as a best practice. It's so incredibly wasteful.
- lmm 3y agoWhat exactly is it wasting? Is your screen going to run out of ink? Even in the physical contruction world, people often build as much or more scaffolding as the thing they're actually building, and that takes time and effort to put up and take down, but it's worthwhile. Sure, maybe you can do everything you would do via TDD in your head instead. But it's likely to be slower and more error-prone. You've got a computer there, you might as well use it; "thinking aloud" by writing out your possible API designs and playing with them in code tends to be quicker and more effective.
- chris_wot 3y agoIf you’ve refactored code and a bunch of tests fail, then you’ve likely introduced a bug.
- chris_wot 3y agoNot sure why I’m getting downvoted so badly, because by its very nature refactoring should t change the functionality of the system. If you have functional unit tests that are failing, then something has changed and your refactor has changed the behaviour of the system!
- tsimionescu 3y agoIt is very common for unit tests to be white-box testing, and thus to depend significantly on internal details of a class. Say, when unit testing a list class, a test might call the add function and then assert that the length field has changed appropriately. Then, if you change the list to calculate length on demand instead of keeping a length field, your test will now fail even thought the behavior has not actually changed. This is a somewhat silly example, but it is very common for unit tests to depend on implementation details. And note that this is not about private VS public methods/fields. The line between implementation details and public API is fuzzy and depends on the larger use of the unit within the system.
- zmgsabst 3y agoThe behavior has changed: Checking length is now a function call and not a cached variable — a change in call signature and runtime performance. Consumers of your list class are going to have to update their code (eg, that checks the list length) and your test successfully notified you of that breaking API change.
- tsimionescu 3y agoThen any code change is a breaking API change and the term API is meaningless. If the compiler replaces a conditional jump + a move with a conditional move, it has now changed the total length of my code and affected its performance, and now users will have to adjust their code accordingly. The API of a piece of code is a convention, sometimes compiler enforced, typically not entirely. If that convention is broken, it's good that tests fail. If changes outside that convention break tests, then it's pure overhead to repair those tests. As a side note, the length check is not necessarily no longer cached just because the variable is no longer visible to that test. Perhaps the custom list implementation was replaced with a wrapper around java.ArrayList, so the length field is no longer accessible.
- deleted 3y ago[deleted]
- OJFord 3y agoI don't think that's TDD's fault, that's writing a crappy test's fault. If you keep it small and focussed, don't include setup that isn't necessary and relevant, only exercise the thing which is actually under test, only make an assertion about the thing you actually care about (e.g. there is the key 'total_amount' with the value '123' in the response, not that the entire response body is x); that's much less likely to happen.
- osigurdson 3y agoI think there can be some value to using TDD in some situations but as soon as people get dogmatic about it, the value is lost. The economic arguments are hard to make. Sure, writing the code initially might cost $X and writing tests might cost $1.5X but how can we conclude that the net present value (NPV) of writing the tests is necessarily negative - this plainly depends on the context.