Hacker Newsnew | past | comments | ask | show | jobs | submitlogin

tl;dr: TDD is not, on its own, effective. If code is highly coupled, tests become highly coupled and a nightmare. The author advocates that learning to properly refactor and create low-coupled code should be a first priority ahead of following TDD blindly.

IMO, nothing really profound here.



Very true. Even Robert Martin himself says that TDD without refactoring is worse than TDD (in one of his books). He mentions that if refactoring is not done you will be worse off than had you not done tests at all. He even recommends simply deleting all the tests.


It's interesting where this leads you though, deep into dependency injection, inversion of control containers and patterns like MVVM.

I'm not saying these things are bad but they do have a cost.


It doesn't necessarily lead you there at all. "Dependency injection" is a fancy phrase for "pass stuff a function needs into the function", only it does so by adding state to objects where that state probably shouldn't exist. You don't have to faff around with IoC or MVVM to properly isolate dependencies; indeed, it's the core idea behind something like Gary Bernhardt's "Functional Core, Imperative Shell" (which, if you're not understanding this instinctively, makes me fear for your code):

https://www.destroyallsoftware.com/talks/boundaries

https://www.destroyallsoftware.com/screencasts/catalog/funct...


Sometimes state is an actual desirable thing and strategy can be a dependency.

Heck, higher order functions (functions taking functions as argument) are dependency injection personified.


State is a desirable thing in the code that wraps your business logic, for sure! Otherwise you're not actually doing anything. That's distinct from state in your business logic, though.


That's interesting although after a brief look it just looks like he is splitting the code into two parts, one part he tests and the other part he doesn't. I don't really disagree with this, my UI code has very few tests.

"which, if you're not understanding this instinctively, makes me fear for your code"

Thanks for the insult.


Sorry--that was a generic "you" there, not you-specifically. I'm saying that this is something everybody needs to understand because writing state-munging code is intrinsically harder to do correctly.

He's not just "splitting the code into two parts", though. He is consciously and specifically defining a core based on functional principles that avoid mutating state (which is harder to measure and reason about). That UI layer, his "imperative shell", is where state is managed, rather than embedding side effects into his business logic. (If you watch the Boundaries talk you'll see he frames larger applications as sets of functional cores surrounded by a layer of imperative routing between them.)


"Functional core, imperative shell" sounds quite dogmatic and not "getting it" is no reason to fear for anyone's code.

Just like the OP article argued that TDD can become an all-solving hammer in the eyes of people, so can functional programming. In this particular case I can think of a whole host of applications where the core should definitely not be functional (and, for what it's worth, immutable state is not inherent to functional programming, or at least it didn't use to be). If it were so, ORMs wouldn't be a thing.


I can't think of a single case outside of a systems or game development context (and a number even there) where anything I have written was improved by tightly coupling state mutation to business logic. Not one. Rather than trying to invoke "dogma" so very seriously, feel free to suggest one. But it's a really long row to hoe: if you are conflating state mutation and business logic, you have entire classes of errors that clean separation doesn't and it's harder for you to meaningfully test for correctness. Not firing that gun into your foot seems very obvious.

And functional programming has never encouraged mutation of data model objects. Not at any point. Mutation of data model objects is and can be nothing but a side effect of a function. There are cases where you can't not mutate a model, such as when that refers to a system resource (a console, a socket, etc.); those are not your data model. Those are dependencies that your application uses to create instances of your data model and feed them through a set of functional processes. Almost like there's an imperative shell that deals with and controls side effects, wrapped around a core of pure functions that manage stateless transforms based on your business logic...?

ORMs (in the mutating, own-your-object sense like an Active Record model) shouldn't be a thing because they're awful creations that aggressively they encourage bad, muddy code that's harder to test and trace and debug. Once more for emphasis: the second you mutate a data object you have made testing an order of magnitude more difficult and now you have to live with that forever. Active Record requires this. There are places where you may choose to embrace this, but it's a good way to shoot yourself in the foot.

On the other hand, data mappers (Anorm, DataMapper, Slick) live outside of the cleanly tested, no-dependency core (and its data models) and are used to feed objects into your business logic and record the results back to an external data store.

If you aren't using plain old objects to encapsulate your data, you're asking for it.


You know what? I had prepared a longer answer addressing most of your opinions here, but I'd rather not[1] so I'll just say I disagree and leave it at that.

[1] After all, I can't conclusively prove an imperative codebase is more maintainable anymore than anyone can do it for a functional one so this would just be another entry in a 50-year-old flamewar.


You might not be "conclusive", but a cogent argument like "I'm adding classes of bugs that don't exist otherwise but I get X for doing so" should be pretty straightforward. If it exists. My contention is that it doesn't, but I'm always willing to change my mind. I'm pretty emphatic about this because I've spent twenty years learning that every other approach in common practice will eventually screw you catastrophically.


> If code is highly coupled, tests become highly coupled and a nightmare.

TDD is supposed to prevent that. What's supposed to happen is, if the test is hard to write, it's telling you something. You're supposed to pay attention to that pain. It's supposed to show you that your code is highly coupled, and make you fix that.

If you just go ahead and write the test, difficult as it is, instead of finding a way to refactor to clean up the coupling, then yes, TDD produces a nightmare of a test. That's because you're not doing it right, though.

(Yes, I recognize that this is a lot like a No True Scotsman. But in fact any methodology, practiced badly, will produce sub-optimal results...)




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: