7 ms·
Any fad historians here? I got into the game late, but my impression is that TDD peaked when untyped interpreted languages like Ruby and JS were en vogue. A lo
by jim-jim-jim 2y ago
Any fad historians here? I got into the game late, but my impression is that TDD peaked when untyped interpreted languages like Ruby and JS were en vogue.
A lot of the maintenance/brownfield benefits the author cites can also come from the compiler itself if your language has branded types, ADTs, non-empty collections, and whatever else. I feel like you shouldn't bother testing what you can already prove.
None of this is esoteric. It can be spun up in TS, but mass culture is lagging behind technology.
- bazoom42 2y agoStatic typing is great for other reasons, but it doesn’t replace any tests. If you write a square root function, static typing can just ensure that it always returns a number which doesn’t tell you much. Unit tests can ensure it returns the correct number at least for some inputs. This is a lot more useful for ensuring correctness. The counter argument is that tests donsnt ensure the function always returns a number. Maybe it returns a date or a string for some particular input which is not covered by the test? While this is true, in my experience this is not a common problem.
- mmcromp 2y agostatic typing doesn't cover all (or most) testing but it absolutely replaces type testing. webdev isn't square root functions it's working with complex data APIs. In fact, the argument was about ruby/rails not needing types/interfaces was because "the tests are the interface" that's how baked in tdd was
- kazinator 2y agoIf I have a dynamically typed add function and I test that add(2, 2) produces 4, then the type testing is rolled right into it trivially. 4 has a type. If an object comes out of add(2, 2) that isn't an integer, then it won't match 4. Oftentimes, type is just a less specific match. It would be a waste of time to write a separate test case that add(2, 2) produces an integer, rather than just testing that it produces 4. I've hardly ever seen Lisp test cases that look only at the type of an output, rather than a specific value. Even a function whose job is to operate on types actually has inputs and outputs that are values representing types; e.g. the symbol string representing string types or whatever. However, there are tests which show that a function signals an error when given wrong types. Those would not be necessary in a static language; the function called with wrong types would not compile, and it goes without saying that all tests must compile, unless we are testing the compiler.
- t-writescode 2y ago> but it doesn’t replace any tests. As someone who has programmed in statically-typed languages and ones that aren't, I can absolutely, positively confirm that a static typing system and compile-time checks absolutely, positively replace an enormous swath of tests. The difference between refactoring code in Ruby and in Kotlin (or Java or C# or whatever other language you choose) is worlds apart when you can actually trust that the thing being sent to the method you're checking actually conforms to a certain type. What would take multiple days and maybe weeks to find all the edge-case-hells of type assumptions in Ruby is often a multiple-minutes change in those other languages.
- rvz 2y agoThis. Unfortunately, this industry (especially startups) embraced the low-end of several untyped programming languages that are used in production-grade systems and evidently there's a price being paid by those choices. Java, Kotlin or even C# would be a much saner list of statically typed languages to use and testing code written in those languages is much easier to reason with.
- bazoom42 2y agoIf you wrote a test which only checked that sqrt returned a number (any number) for a given input, then that test would indeed be superflous with static typing. But my argument is you would not write such a test in the first place. You would write a test which checks the output is correct which obviously implies it is also a number. Static typing does not in itself check if the output is correct, so you still need the test.
- t-writescode 2y agoI think the thing most people are arguing with you about here is the distinction between, "it doesn't replace any tests" and "it doesn't replace all tests". Of course there will still be tests, but as other responses have been implying, there are entire classes of bugs that are runtime in Ruby and friends that are compile-time exceptions in other languages and those bugs would need to be weeded out in tests in Ruby, but they don't need to exist at all in other languages. And, there are huge qualities of life and developer velocity benefits to the statically typed languages due to those.
- jim-jim-jim 2y agoI'm not arguing against tests as much as I'm suggesting that sufficient typing can provide a similar form of interactive refactoring that the author claims TDD facilitates. Numerical functions are usually good candidates for tests. I don't even have an axe to grind about TDD itself. I don't do it and nobody forces me to. I'm just hypothesizing that the practice was tied to the primitive typing of its time. I could be wrong about this.
- motorest 2y ago> If you write a square root function, static typing can just ensure that it always returns a number which doesn’t tell you much. What does your square root function return if you feed it a string? And what does it return if you feed it a JSON object?
- bazoom42 2y agoIf this is a strongly typed “duck typed” language, presumably it will throw an error when you apply some mathematical operation to a non-number value. If it is JavaScript you probably get NaN. If you have code which pass a json objects to a sqrt function then the calling code is in error so presumably tests for that code would flag it. Counter question: What happens in a statically typed language if you pass a negative number? I just tried in C# and surprise: NaN
- motorest 2y ago> If this is a strongly typed “duck typed” language, presumably it will throw an error when you apply some mathematical operation to a non-number value. The value added by statically typed languages is that your runtime errors are instead compile time errors what are checked for free without having to add a single test.
- bazoom42 2y agoYou still need the tests to ensure the code is correct though. A sqrt function returning 27 for every input would be “proven correct” by static typing.
- motorest 2y ago> You still need the tests to ensure the code is correct though. You do, but not the boilerplate stuff to check that your code throws a runtime error if you pass a JSON doc to a square root function. Having runtime errors instead of compile-time ones is also a major liability. > A sqrt function returning 27 for every input would be “proven correct” by static typing. That's not what's being argued.
- makeitdouble 2y ago> what you can already prove. bazoom42 touches on the value testing aspect that goes beyond the mere type or return. Typing and compilers also only guarantee coherency inside your declarations. If for instance you're expecting some library to return an int, but it's utterly broken and actually tries to return a string, the strong typing system will only let you identify why it's crashing in production. Hopefully you'd want to detect this class of issues beforehand.
- HideousKojima 2y ago>Typing and compilers also only guarantee coherency inside your declarations. If for instance you're expecting some library to return an int, but it's utterly broken and actually tries to return a string, the strong typing system will only let you identify why it's crashing in production. Hopefully you'd want to detect this class of issues beforehand. If I try to call a third party library method in a language with a sane type system (like C# or Rust) that returns a string and attempt to assign that value to an int, it won't even compile. And I'll get a red squiggly line before I even try to compile. What you're saying would only apply in something like Python or TypeScript where you might be using types/type annotations but the library you're calling isn't.
- makeitdouble 2y ago> If I try to call a third party library method in a language with a sane type system (like C# or Rust) that returns a string and attempt to assign that value to an int, it won't even compile. You're assuming that for instance the library is correctly setup and won't crapout because a system requirement is not met. Or that no other external factor will interfere with the result (e.g. your library is hooked to an API) Compile time checks will be against a declared interfaces, not the actual library built outside your project. To me that's the kind of issues where having a test coverage really helps, a lot more than just checking that mocks and interfaces match each other (to show my hand, I prefer dynamic languages, so interfaces will rarely fail and many edge cases can be simply be coerced into a generic path, so the real scary stuff is when the rubber meets the road and expectations meet reality)
- rvz 2y ago> Any fad historians here? I got into the game late, but my impression is that TDD peaked when untyped interpreted languages like Ruby and JS were en vogue. That was one of the first mistakes that began to spread everywhere and new engineers bought into the fads of using such unsound languages like JS, Ruby where it was difficult to reason with the type system to the point where they were testing the expected types themselves. In some applied uses such as backend systems, the fundamental design of untyped / dynamically typed languages can just let down the quality of the software in the first place and be a massive burden and cost on the developers to the point where they MUST use TDD no matter what even in greenfield projects. TypeScript has a more sound type-system, but still just serves as a temporary solution to the fundamental issues JavaScript always had and its still very easy to escape the type system and break it at runtime.
- solatic 2y agoAt the end of the day, people need to do two things: write code that powers the solution, and write code that proves that the first code was correct. There are so many different ways to write the second - not just automated tests (be they unit/integration/service/component/e2e tests) but also strong static types (for earlier feedback from the compiler) and least-permissions security (for later feedback to catch unforseen behavior in production). There's no replacement for educated engineers who pick the right tool for the job. If you go too heavy on static types, you get Haskell and Idris and get a lot of difficulty training people past the learning curve and actually putting value-generating code into production. If you go too heavy on automated tests, you get test suites that take hours and hours to run in CI and are extremely brittle and difficult to change. If you go too heavy on security infrastructure, you get big bank style practices where you're lucky to put anything past the review committees / bureaucracy.
- voiceofunreason 2y agoWell, TDD (or its immediate precursor, depending on where you draw the lines) escaped from the Smalltalk world circa 1997; but I think you can make a case for 1999 being when it really began to emerge. Most of the examples that I saw in the next 5 or so years were written in Java or Python. Beck's book was 2003, with examples in Java and Python. David Astels wrote a book later that year, again primarily Java but also with short chapters discussing RubyUnit, NUnit, CppUnit.... Growing Object Oriented Software was 2009. My guess is that "peak" is somewhere between 2009 and 2014 (the DHH rant); after that point there aren't a lot of new voices with new things to say ("clap louder" does not qualify as a new thing to say). That said, if you're aware of the gap between decision and feedback, and managing that gap, I don't think it matters very much whether that feedback comes in the form of measurements of runtime behavior vs analysis of the source text itself. It might even make sense to use a mixed strategy (preferring the dynamic measurements when the design is unstable, but switching strategies in areas where changes are less frequent).
- kazinator 2y agoTDD is applicable to anything. C, C++, Java, Rust, ... If you're touching the code in any way, there must be a way of writing a test which fails if you don't touch it in that way and then passes if you do. It's not necessarily just about ensuring correctness of new changes. What if you're wasting your time with that change? I.e. the test you want to pass after your change, maybe it passes already, so the change is useless. If you write it first, you will know. The TDD test is itself compiled, and so the language features/checks help write the test. Then, if the proposed change doesn't compile, that's a fail? The compiler is helping again.