Readit News logoReadit News
JohnBooty · 2 years ago

    Over the years I have realized that some comments are 
    needed and useful. These days I add comments when there 
    is something particularly tricky, either with the 
    implementation, or in the domain.
Ah, yes. Another convert. There are dozens of us. Dozens!!

Nothing makes me feel crazier than trying to get fellow engineers to comment their code. Code alone can only tell you the "what." If the "why" is not obvious, comment it.

    Unit testing private methods. 
I am always dismayed by engineers who vote a hard and unyielding "no" on this issue.

I guess ideally your private methods should not be tested directly. If you work at an ideal shop doing ideal things under ideal conditions, please let me know if you are hiring.

In all or most of these cases the tests for private code will hopefully be somewhat temporary; perhaps think of them as scaffolding used during construction or renovation.

- For example, perhaps you have a good reason to write the private methods first and you would like to make sure they are sound before proceeding.

- Perhaps you have a division of labor due to a time crunch and you are writing the private methods while somebody simultaneously writes the public methods.

- Perhaps you are encountering some thorny preexisting code with no test coverage and you would like to just make sure things work.

- Perhaps the public methods are undergoing a lot of flux and you would like to make sure the private methods do not suffer regressions during this flux

- Sometimes it's just easier to test private methods directly rather than indirectly via a public interface. Maybe this is a code smell, but also maybe you don't have time for a full refactor.

Yodel0914 · 2 years ago
I'm still firmly in the "no" camp on testing private methods. By definition, anything your class does in the real world is done via its public interface; I'm not sure why I need to care about what's done under the covers.

I've actually moved further and further away from unit testing over the years (after being a pretty big TDD fan for a long time). In terms of bang for buck, integration tests across your public API are the best IMO. You're testing how your API is actually used. The problem historically has been that they're fragile to refactoring and difficult to run, but with the right tooling you can get around that.

turtles3 · 2 years ago
I find the opposite wrt. refactoring - that is to say integration tests are _more_ robust to refactoring than unit tests, and that's one of the reasons I strongly prefer them.

Unit tests are deeply coupled to the internal structure of your system - refactoring often implies changing unit tests, which opens the door to bugs where the code and test both change to match each other.

As you say, integration tests validate your public API, which from a 'correctness' point of view is really the only thing you care about, not the internal structure of the system. That's why I love integration tests, you can make sweeping refractors without needing to change the tests, because the test will still tell you whether the behaviour of the whole system is correct.

audunw · 2 years ago
I’m wondering if the software world has something to learn from digital design here (for once), where there’s a huge emphasis on code coverage collection.

You really shouldn’t care how a function is tested. That there exists a test for that particular function isn’t particularly useful measure of how well the function is tested. So you need a methodology that tells you which of your functions have been covered by your tests. If it’s not covered you should make a judgement about how to get that coverage.

Getting your private methods covered by the public interface is good because it encourages you to write thorough tests for those public methods that covers all the ways the method might be used.

JohnBooty · 2 years ago
First thing I'd say is that there's no "one size fits all" approach. The right answer depends on the language, the project, the requirements, the amount of churn, the timeline, etc. So I 100% believe you when you say you've found success by moving the focus toward implementation tests. =)

But in my experiences, I've had major issues with a reliance on integration tests. This is almost exclusively in Rails.

- They're slow, because there wasn't an easy way to stub out the slow stuff

- If you stub out the slow stuff now you have no coverage for the stubbed stuff, unless you also have unit tests covering the guts of the app

- When integration tests fail, it can take a lot of work to investigate the cause, as opposed to granular unit tests

jasfi · 2 years ago
Agreed, there isn't unlimited time to perform testing. Sometimes you also know when you're writing tests that will never help find a bug, and that's a waste of time.

I always focus on integration testing. If I have time I'll write unit tests, but there needs to be a good reason. I'll unit test an area that has proven to be buggy even with integration tests, but this is rare.

Finally, some code is just really complex and/or critical. These are good candidates for unit tests.

appplication · 2 years ago
Integration tests are great, but the very real downside is that they tend to be much more complex to write than unit tests, and can in some cases require setup and tear down overhead that makes it impractical to lean on them for the majority of testing logic.

Unit tests for each unit of functionality, and a small number of integration tests to validate all works well together.

kaba0 · 2 years ago
I think unit tests can be divided into two camps as well: there are the trivial ones that are very close-coupled to the implementation, like, check if my ad hoc stack implementation will actually add the element and return sensible outputs. Something like this can easily be a private method as well, and I think they make sense to include right next to the function itself. Some languages allow these reality checks right in the documentation itself - and I don’t see who would it hurt. They are closely coupled to the implementation so sure, they will have to be rewritten with it, but that’s completely fine.

The usually called unit tests indeed should not directly test implementation details, like private methods, but then we have to find a different name for the former because I do think they are useful sometimes

moring · 2 years ago
> Unit testing private methods.

Adding the single biggest reason I have encountered: Because the overall functionality is too complex to test through the API alone, so the alternative is to extract part of the "private" functionality to a separate class that would then be public itself, making functionality public that shouldn't be. By that, the discussion about "testing private methods" ignores the elephant in the room, which is that the language used doesn't have a rich concept of public vs. private in the first place.

As evidence for that I'd like to mention that I never had that problem in Rust, because I can simply extract the private functionality into helper functions within a private sub-module and unit-test them there, without that functionality becoming public to the rest of the code.

sverhagen · 2 years ago
If your class is doing so many things that you can't reasonably test everything but through the private methods, how about extracting a helper for those things which has testable public (or package private, if you have to) methods!? Did I just sneakily bypass the rules, or make the code better overall? I don't know anymore...
BugsJustFindMe · 2 years ago
> If your class is doing so many things that you can't reasonably test everything but through the private methods, how about extracting a helper for those things which has testable public

When your solution to not wanting to write tests for important behavior is to trick yourself into wanting to by adding extra layers of code for no other reason, maybe the rule stopping you from doing it was wrong.

boxed · 2 years ago
It's not about that imo. It's about having tests that are limited in scope as much as possible, so if they fail it's much easier to understand the problem.

Ideally tests should be run in a specific order, from most low level to most high level. So tiny basic functions first, then functions that use those functions, then functions that use THOSE functions, etc until you have end-to-end tests.

Unfortunately I know of no testing system that has such a hierarchy.

JohnBooty · 2 years ago
I agree that if your private functionality has grown complex, that's a strong "code smell" that perhaps it needs to be extracted.

But yeah, then you've changed it from private to public.

SenAnder · 2 years ago
Why you should test private methods: To make sure they work correctly. This may be significantly easier than testing the public methods that use them.

Why you shouldn't test private methods: Because the implementation might change, obsoleting the tests. But everything might change (except the end-user requirements, haha), so by this argument you shouldn't test anything except what is in the spec/what the user will see.

The only benefit of not testing private methods, is the same benefit that not testing anything else brings.

sanderjd · 2 years ago
I've actually become a bigger fan of unit testing private methods over time. :shrug:
Groxx · 2 years ago
Yeah - the alternative is often to make your non-test code worse to make it more testable.

Test private methods when you have sufficient behavior in them that it's worth testing in isolation.

mkoryak · 2 years ago
If you don't have time to write good tests, maybe consider not writing any tests. I've worked for many a startup where we didn't have any tests because we rewrote everything every few months.

And that was ok because everyone was happy to have a lot of shit written fast.

I guess maybe what I'm trying to say is that shitty tests are sometimes worse than no tests.

JohnBooty · 2 years ago
I don't agree that private method tests are shitty.

However, I do agree that maybe there is a time during which it's OK to just not have tests. If you are just spitballing it, prototyping, code jamming, maybe even blasting out an MVP for alpha or beta testers. Sure.

Part of mastering a craft is making reasoned decisions about breaking rules.

naruhodo · 2 years ago
I have given up on being annoyed or proselytising. It's much less stressful.

I write the documentation that I would like to see myself.

mcv · 2 years ago
> Code alone can only tell you the "what." If the "why" is not obvious, comment it.

But isn't that the common view? I think most programmers would agree that the "why" deserves documenting. The problem is that in the heat of the moment, most forget.

thom · 2 years ago
Just to intensify the sacrilege here: I’ve been happy testing private behaviour with tests directly alongside the code. This way you can easily sweep away the test code if you change the implementation, and it serves as decent documentation for complex implementation details. The tests obviously disappear in production builds either way.
JohnBooty · 2 years ago

    I’ve been happy testing private behaviour with tests 
    directly alongside the code.
I'm intrigued but I don't understand. What does this look like? What language is this?

oweiler · 2 years ago
No one has ever said that you shouldn't comment your code.

"Comment the how, not the why."

JohnBooty · 2 years ago
When you read software engineering books like Clean Code, they are generally heavily in favor of writing good comments.

When you read programming books, like "Learn Language XYZ" books, there are generally no comments whatsoever because there is no need. Which makes sense in the scope of such books, but I think it accidentally sets a precedent for eschewing comments in the minds of many.

But.

Out in the "real world?"

I'd say 90-95% of coders don't comment a damn thing. At least that has been my experience working in the industry since the 90s.

Some coders are in the obnoxious and toxic "code should be self-explanatory" camp, which is objectively dumb. Code can't explain intent, such as business rules or weird shit you're doing to work around weird shit in external libraries or APIs or hardware bugs.

A greater number of coders are of the mindset that inline comments are bad and that things should be explained in the commit message and/or pull request. This is more noble, but I think it is not nearly as practical as inline comments for a variety of reasons.

Many coders also believe that comments make code harder to read. I find this baffling and dumb. Get a bigger monitor or get an editor that lets you collapse/hide comments with a keystroke.

diarrhea · 2 years ago
A great deal of people have. Uncle Bob and his substantial following, for example.
marklubi · 2 years ago
I have a tendency to comment code that isn’t optimal and will need to be refactored for performance at a later date.

Not sure how many times I’ve been looking into a new performance issue as we grow, and come across one of my own comments.

Sort of falls into the Good/Fast/Cheap trifecta. When you have to do the Fast/Cheap bit, note when you know it’s not optimal so you can identify it quickly when you or someone else comes back to it.

iamflimflam1 · 2 years ago
Unfortunately there is/was a very loud group that said exactly that. The opinion was that code should be self documenting.
rustybolt · 2 years ago
I have heard many people say this.
mtVessel · 2 years ago
That's backwards. The code is the how, the why is what comments can add.
jader201 · 2 years ago
I don’t think you shouldn’t comment your code, but in my experience, good code doesn’t need much, if any, comments.

This is because I define “good code” as simple code. If code gets to the point that it needs to be explained with comments, then there’s a good chance there is opportunity for simplification.

In other words, if I can’t read code and follow what it’s doing — or why it’s doing it — then I won’t ask them to add comments, but to instead refactor/simplify the code. (This is assuming the code can’t be made more readable with simple name changes, which a lot of times is all that is needed.)

Yes, there are times when the “why”, even with simple code, needs to be explained. Comments in this case are fine. But ideally this is the exception and not the norm.

deergomoo · 2 years ago
I’ve never understood why anyone would choose to debug by printing and logging if they have a good debugger available to them.

My day job is mostly PHP (yeah yeah, I know) and the general attitude towards Xdebug in the community seems to lie somewhere between apathy and active avoidance.

To me, a debugger is such an invaluable tool not only for identification of bugs but also for familiarising oneself with new code. The value of being able to not only trace execution from start to finish but also to modify state in a running program is immense, and gives me a small taste of what Lisp folks rave about.

Littering print statements everywhere feels like banging rocks together when there’s a perfectly good lighter in your pocket.

etler · 2 years ago
It depends on what you're doing. I find console statements helpful when testing out a new function that you don't necessarily expect to misbehave so I check the internal state is correct without having to stop the code.

Debuggers provide much more information, but stopping and stepping through the code is much more time intensive than printing out variables at various points and running the function and doing a quick double check that they're correct.

It also allows you to check the state of the program earlier for problems that may only emerge after the fact. Instead of having to stop what I'm doing and retry and step through with the debugger, I can just look back at the log and see what went wrong where.

I think both debug logs and debuggers have valuable use cases.

tincholio · 2 years ago
Tracing debuggers give you the best of both worlds. I've recently started using Flow-storm [0], by @jpmonettas), and it's been quite transformative. You can still easily see the values flowing through your system (better than just "prints"), and it can handle multi-threaded / async scenarios quite nicely. You don't need to manually step through code, you can just "see" your data flow, and when you have loops or some other form of iteration, you can see the data for each pass. Coupling this with a good data visualization tool (such as Portal [1]) really feels like magic. I've been doing Clojure for quite a few years now, and was very happy with my plain REPL-driven workflow, but this is way better.

[0] https://github.com/jpmonettas/flow-storm-debugger

[1] https://github.com/djblue/portal

bluefirebrand · 2 years ago
More and more software, especially on the web, is asynchronous microservices.

Debuggers are basically useless for both of those imo.

Debugger break points introduce synchronous behavior into your async application, which basically forces race conditions to fail when debugging.

And I'll admit to not being a debugger expert but I don't know of any that can watch multiple executibles at once.

KRAKRISMOTT · 2 years ago
Or God forbid those Next.js apps where the same line of code is ran on the server, streamed to the client, and then ran again on the client via rehydration. 3 different potential sources of failure and you have to debug them simultaneously.
randomdata · 2 years ago
There shouldn't be a need to connect to another service during debugging except maybe to validate that an external service stands by its published contract. A debugger (nor printf) does not seem like the right tool for that job, though.
flohofwoe · 2 years ago
This just means that debuggers have to catch up. Anything that runs on a CPU is also debuggable, and if it's too much hassle, the debuggers need to improve. Debuggers are much more than just stepping through code and setting breakpoints, they are (or should be) "interactive program state explorers and visualizers".

(IMHO no programming language should add features which make interactive debugging harder, it's never worth it in the long run)

fulafel · 2 years ago
There are various kinds of debugger. For investigating systems like this I think you want a trace / time travel based debugger, like eg FlowStorm in the Clojure world.
The_Colonel · 2 years ago
I don't understand the problem, why can't you simply run the debugger of your choice multiple times, each for one process?
sanderjd · 2 years ago
IMO, this is one of the reasons that architectural style is not actually very good...
jandrewrogers · 2 years ago
I use both, they have different purposes. I find printf-style debugging to be much faster for bisecting the control flow, and the vast majority of bugs I see are of this type.

Debuggers tend to be more useful in cases of corrupted state that isn't the result of broken control flow logic. These are much rarer, usually the product of something like broken bit-twiddling logic. And these days that kind of code is rarely written manually. When I write a non-trivial data container, I usually provide a standard public method that does a deep inspection of the integrity and consistency of the internal state. These are much too expensive to use as a typical assertion, but turning this on usually isolates the problem so well that a debugger isn't required to figure out what went wrong.

I used to use debuggers a lot more than I do now because the nature of the typical bug has changed. The types of bugs are also much less diverse than they used to be.

Hermitian909 · 2 years ago
One of the great UX features of printing is that it is a quick way to have an auditable form of state over time. I look at states 1-n, compare and contrast, which my debuggers don't make as easy.

I'd add many people don't work in languages with good debuggers. I work in TS and the debugging async node code is an absolutely garbage experience in many cases.

whynotmaybe · 2 years ago
It's because both are great for specific tasks.

If I want to check that the dependency injection engine creates & disposes correctly while navigating the app, I only need printing.

If I want to intercept some process and check value on the fly, or even change it, debugger is the way to go.

It's kinda ironic that you're discarding the matches because you have a good lighter.

beagle3 · 2 years ago
For many nontrivial debugging tasks. You need to record a lot of past context - e.g. you can put a breakpoint when a bad decision gets made, but the data causing this decision likely accumulated over a few tens/hundreds of previous iterations - and in particular one bad iteration that you don't (yet) know how to characterize. In such a case, setting up watches and pressing F5 a hundred times to get to bad iteration is inferior to adding a print/log and scrolling back to it.
flohofwoe · 2 years ago
It's not one or the other, most debug sessions will also include some sort of adhoc logging.

Also "modern" debuggers ("modern" as in "this century") offer a lot more than just simple on/off breakpoints: conditional breakpoints, data read/write breakpoints (e.g. who wrote this broken piece of data), "tracepoints" which don't stop execution but instead log out a message or run some scripted action, etc etc...

A good debugger is essentially an interactive program state explorer and visualizer, ideally it would be fully integrated with the actual development process (e.g. edit, compile, run, break somewhere to inspect program state, tweak some code while program is suspended, rewind a program state a bit and continue running with the modified code, rinse, repeat...).

anthonypasq · 2 years ago
you can make a conditional breakpoint
theshrike79 · 2 years ago
You don't need to litter print statements, you can litter log statements :D

I can't even remember when was the last time I was on a project where debugging would've been even a bit doable. Maybe, just maybe, during development when it's running on my computer.

But it worked on my computer, that's why it ended up in production =)

With proper logging (or just print statements) I can go in after the fact and narrow down what happened when it didn't work in a specific situation.

Topgamer7 · 2 years ago
Having good log statements about what is being called with what value is invaluable when running a server and using log output to investigate issues.
accoil · 2 years ago
They also let you troubleshoot a problem after it has happened. Logs can give information about the trigger, which the debugger can't do as it requires you to known what inputs caused the issue.
smusamashah · 2 years ago
Can you please explain how does debugger can help in cases where you can't wait too long on a breakpoint because some thread or service waiting on it will fail? I do this many times at work in Java with intellij. I do fine with breakpoints most of the times but sometimes print statements is the only way to trace the thing.

From your comment I feel like I am missing better ways to debug.

Gibbon1 · 2 years ago
Thing that annoys me is I know that debuggers could do a quick grab and dash.

Either stop the program snag the state of a variable and go. Or set a sample point where when it gets hit the debugger takes a sample of local state and continues. It aught to be able to do that within a few hundred clock cycles.

Myself I use error/debug printf, a command line interface, debugger, and sometimes an oscilloscope.

With error/debug printf I leave them in and just turn them on/off with a compiler switch. Over time they tend to evolve into something that provides good info on what's going on.

The command line interface allows me to inspect the program state on the fly and run tests on the fly.

The debugger allows me to do random look sees of what the program state is and follow the call tree.

Oscilloscope allows me to inspect/debug/verify timing issues.

My suggestion to you is implement a command line interface to you program so you can interact and inspect your program while it's running. It's really not hard at all. Implement commands as a table. { "command", handler, extra_data}

deergomoo · 2 years ago
Are you able to also set a breakpoint in the service that’s waiting, such that it is also paused?

I’m really not familiar with Java but given PhpStorm supports concurrent debugging sessions I assume IntelliJ does too.

Groxx · 2 years ago
Use a log breakpoint? No need to recompile then, just add the breakpoint and do the thing again.
knubie · 2 years ago
Print debugging can be much faster than using a debugger if you can figure out the problem and fix it on the first try. For more complex issues it makes sense to switch to a debugger.
flohofwoe · 2 years ago
You really shouldn't have to "switch to a debugger" as if it were a separate tool, but instead have it integrated into your development environment (e.g. "run" or "step into" should both be just key presses, and every "run" automatically runs inside the debugger so that you can pause and inspect program state at any time).
boxed · 2 years ago
I don't understand how that is possible. I would say the exact reverse. With print debugging you have to write the prints. With a debugger you just run the code. Especially with conditional breakpoints...
pkolaczk · 2 years ago
> I've never understood why anyone would choose to debug by printing and logging if they have a good debugger available to them.

Because logging provides a different experience and can be sometimes much faster. I can run the code once and then analyze all the collected information later. The log allows me to go back and forth very fast, or very quickly see that e.g. a 150th iteration went sideways even if I don't exactly know what to expect. Another area where logging is tremendously helpful is debugging concurrency related bugs.

josephg · 2 years ago
Yep. Most debuggers also make it very hard to go backwards in time. Logs can be read in any order.

There’s a neat trick I like to do when I’m trying to triage a bug that only shows up with some input data. I’ll run the passing test and the failing test with detailed logging, then diff the logs to understand how the code is behaving differently in each case.

Supermancho · 2 years ago
A jr programmer once asked how I know that N lines do what I expect without writing a test every 10 minutes. Logging is a form of unit testing. Logging can be thought of as unit tests for your specific context, decoupled from the logic (almost) entirely.

I have an expectation of state, the execution can be checked against that expectation via observation of the output (which usually has serialized state). It's not a fancy True False or checkmark, but it's an assertion (which constitutes a test of an expected state) nonetheless.

catiopatio · 2 years ago
There are two junior developers in your anecdote. That’s not a test. It’s not even comparable to unit tests, much less a replacement for them.
bakugo · 2 years ago
> My day job is mostly PHP (yeah yeah, I know) and the general attitude towards Xdebug in the community seems to lie somewhere between apathy and active avoidance.

From my experience, simply having xdebug enabled comes with a significant performance hit, which is why I generally avoid it unless I really need it.

deergomoo · 2 years ago
I believe that’s mostly solved with Xdebug 3. The performance hit of having it enabled but not actively debugging was greatly reduced compared to 2.
sebstefan · 2 years ago
>the general attitude towards Xdebug in the community seems to lie somewhere between apathy and active avoidance.

Every year more good engineers start refusing to interview for most php gigs if it's not FAANG. You're with what's left of the rest

andrewstuart · 2 years ago
>> I used to think that the names of the classes, methods and variables should be enough to understand what the program does. No comments should be needed. Over the years I have realized that some comments are needed and useful.

Comments are needed where there is more to the code than just reading it. Where there is some external reason WHY. Where it is written in a specific way for non-obvious reasons. Where there are pitfalls and dangers in changing the code. Where this specific approach is the result of fixing some problem and if you change it then you might be reintroducing a problem.

There's lots of reasons to comment your code, but mostly I think code should be the documentation.

The fewer comments the better, because then developers who come later will see comments and thing "this must be important because there is a comment here". Too many comments dilutes the value of comments.

When it really matters I start my comment with a couple of lines like this:

  // LISTEN UP!!!
  // LISTEN UP!!!

I have to say, it seems strange that the author EVER thought that no comments should ever be needed - that seems like a strange and dogmatic conclusion to have come to.

brabel · 2 years ago
> it seems strange that the author EVER thought that no comments should ever be needed

That was a trend many years ago when people started seeing how comments are often out-of-date, sometimes been copied pasted around so many times it's not even saying anything useful about the code next to it, and sometimes the author is just a bad writer and makes it more confusing than the code itself. So people became pissed off and kind of decided that the code needs to be clear enough to be self-documenting: short functions with verbose names, good variable names etc. making comments unnecessary.

I do agree with that, but IMHO there's still a place for comments: as many others are saying: when you need to document why something has been written that way, not just what (which the code should be able to tell by itself)... and I believe OP is also claiming that, and you appear to be missing this context.

zimpenfish · 2 years ago
> it seems strange that the author EVER thought that no comments should ever be needed - that seems like a strange and dogmatic conclusion to have come to.

Distressingly common, though. I'd say most of my last few positions have been at places where "self-documenting code" was the mantra even though it was blatantly clear that the code and what it was doing was far too complex for that to work.

seer · 2 years ago
That gets me thinking, if the code is obvious enough it doesn’t need comments, is it useful code? Like in an ideal world, all the “how” is transparent and we’re left only with the juicy bits, that explain / define business needs.

I’ve noticed that different language have various ideas about this - like

Closure would be close to a fetish to build abstractions so that the actual code that does “stuff” just explains the business process, and the rest are just libraries.

Or golang goes in the other extreme where there it is idiomatic to have as little abstraction as possible, writing up everything as you go.

Haven’t coded much in either so this is just a beginner’s observation.

kaba0 · 2 years ago
Programming language generations may be relevant here if you are interested. Prolog for example can be called a 5th generation PL, which means that you state what you want instead of how you want it achieved. Wikipedia lists SQL at the 4th gen, though I think the former is just as true for SQL as well - you only state that you want this and that row/field, and the actual algorithm is up to the query optimizer.
ben-schaaf · 2 years ago
The juicy bits that define business needs can be quite obvious and thus not need comments.
throwaway290 · 2 years ago
You self-contradict with:

> it seems strange that the author EVER thought that no comments should ever be needed - that seems like a strange and dogmatic conclusion to have come to.

But just before that you say:

> There's lots of reasons to comment your code, but mostly I think code should be the documentation.

Exactly that's why it's not at all strange. As you yourself write, code should be the documentation. Need to add comments => your code is unclear, and unclear code => bad code, and so instead of writing comments your time is better spent improving the code.

The reality of course is that things need to be done and there's no time to fix all layers and have perfectly organized and readable code and therefore clarifying comments are needed.

So no need to shame the author, it's just a typical progression from youthful maximalism to more mature shades of grey thinking.

brianmcc · 2 years ago
I think the "never use comments" trend came from folks who'd realised "comments lie, code never does".

E.g. developers might forget to update comments when they fix a bug.

There are also the "// set b to 0" brigade, which is an exaggeration but makes the point that commenting the painfully obvious is a trap too many fall into.

I think there was also an argument that methods called "setWidgetsToMaximumSoThatStartOfDayChecksWorkOnWeekends()" were a Good Thing, and definitively Self Documenting, a viewpoint which seems (thankfully) to be losing popularity.

JambalayaJim · 2 years ago
My current workplace mandates no comments. It’s very annoying.
ajmurmann · 2 years ago
Another way to address some of the reasons for comments you call out so well, is clear tests for these edge cases or weird, past problems.
mplewis · 2 years ago
I wish that my programming languages had a "public for testing only" scope. Despite the rigorous arguments of the Properly Factor Your Public API crew, I still find myself in real-world situations with functions that should not reasonably be used outside of the module, but that I still find valuable to cover with unit tests.
wavemode · 2 years ago
C++ has "friend" classes, a quirky feature I've never seen in another language. But which solves this very problem.
sanderjd · 2 years ago
It's not much different that package-private (the default visibility in java).
Yodel0914 · 2 years ago
You can do this in C# by labeling something as internal, but available to certain named assemblies, eg: [assembly: InternalsVisibleTo("UnitTests")]
zem · 2 years ago
yeah, that one stood out to me too. i would be tempted to make the stronger statement that it is always strictly better to have your private code unit tested, and it is only the limitations of languages and test frameworks that make people reach for ways to test them through the public API.
DanHulton · 2 years ago
Allow me to even more strongly disagree. I've not yet seen a situation where strongly-tested private code would have helped, although I routinely come across cases where too many tests get directly in the way of a refactor or bugfix that _would_ improve the program.

These days, I'm of the opinion that you should test at the highest level possible (if testing a web service, for example, at the level of the actual public API to the service), thoroughly define the behaviours expected for those APIs, and leave the insides of the service entirely untested, unless you have _very_ good reason to. (Critical code, payment code, hard-to-test code, etc.) This ensures you have the important behaviours tested, but leaves you free to modify implementation details as necessary without needing to alter or rewrite entire swathes of test code that suddenly starts failing because you decided to break up or consolidate an inner method.

pvorb · 2 years ago
But on the other hand: if it's worthwhile testing your private methods, shouldn't they maybe be refactored to a public/package-private scope, so they can be reused?

If you disagree, maybe an illustrative example would help. I couldn't think of one where I want to test a private method in detail that is not worth exposing.

mvdtnz · 2 years ago
This exists (in kind of a hacky way) in C# with the ExternalsVisibleTo attribute. It exposes methods and classes only to specified assemblies (like your unit test assemblies).

https://learn.microsoft.com/en-us/dotnet/api/system.runtime....

gregoryl · 2 years ago
You can actually stick this in your csproj. I'm on a phone, so bear with me, but roughly

    <InternalsVisibleTo Include="assemblyname" />
Which means you can do stuff with msbuild props, like auto including a `$(assemblyname).Tests` into your projects.

no_butterscotch · 2 years ago
This exists in many Java based languages, an annotation above a method: @VisibleForTesting or some such.
magicalhippo · 2 years ago
I put things like that in a separate namespace. If my namespace is Foobar I'll have a Foobar.Impl namespace.

One of the lessons I've learned is that very little needs to be private, and there's a lot of advantage of not having things private.

Putting that in a clearly defined namespace allows access when needed, for testing or workarounds, while conveying the needed "if you use this you're on your own".

jiehong · 2 years ago
I think Java gave 2 to ways to deal with this:

There is “public” within a module, and “public” for other modules. This is not on a per method basis, though.

And you can make a method “package-private”, and you can then test it right a way.

pkolaczk · 2 years ago
Rust has it. Just annotate your code with `#[cfg(test)]`.
MikeTheRocker · 2 years ago
Kotlin has the internal access modifier which can be useful for this
Klonoar · 2 years ago
pub(crate) in Rust somewhat does this.
rakenodiax · 2 years ago
Not to mention cfg(test)
Supermancho · 2 years ago
Go has this. Python has this (_ access). package-private allows you to write tests if the tests are in the same namespace (Java langs).

Class Access Modifiers were for controlling property and method inheritance, which has been bastardized into "CAMs tell you the API", because it's convenient. An object's API can be defined by an interface (following traditional OOP conventions), allowing CAMs to be used for their original orthogonal purpose. In practice, developers don't want to make an interface when they can slap a CAM on it and say "good enough". Some language don't even support interfaces, pushing the practice. So here we are with the wrong abstraction.

Lio · 2 years ago
Ruby also has this. e.g. you can use send to call private methods during testing (and then use static analysis like rubocop to ensure only public_send is used in production).

You also can simulate it in JavaScript by using a closure and a separate package for your "private" functions. By mixing the "private" code into the closure you can make it private in usage but still available to unit tests.

scruple · 2 years ago
> Python has this (_ access).

What exactly is being referenced here?

jcpst · 2 years ago
It would be interesting to know how long the author has been developing software.

For me, this is the kind of stuff I questioned in my first few years. After I productionalized a few real world systems for a business, the whole “dev street cred” thing lost it’s appeal. It wasn’t about some imaginary “dev purity” thing anymore, it was about being efficient and making sure I was contributing to the bottom line.

SoftTalker · 2 years ago
Maybe. I have been developing for over 30 years. I still use emacs, use logs and printf for debugging, and have never even looked at ChatGPT. I'm willing to admit I may be set in my ways but I get my work done.
jiggawatts · 2 years ago
Nearly 40 years here — I exclusively use IDEs and debuggers and have done so since the early 1990s.

Computers aren’t digital pencil and paper. They’re levers for the mind. If the 3 GHz processor just sits there idling, waiting patiently to copy some bytes from one buffer to another, it’s being wasted. It could be checking the syntax, looking up documentation, or chasing memory references in the debugger so I don’t have to.

bsder · 2 years ago
Programmers had a very nice thing called "web search". Then advertising and spam ruined it.

From my point of view, ChatGPT is simply an anti-spam algorithm which is finally restoring "web search" back to being useful.

The problem is that there is no money in that. So, everybody is trying to apply all the ML/LLM/ChatGPT stuff to absolutely anything in the hopes of making some money.

jcpst · 2 years ago
Good point. They’re all just tools and patterns. The actual tools used _could_ save some time, but experience plus the capacity to reason about software is a bigger deal.

The article just made me think of some devs that I’ve worked with that were obsessed with particular tooling (insert comment about a hammer and everything being a nail).

I do keep an eye on emacs. It turned into mainly my org-mode editor, but the v29 article that hit the HN front page recently has me curious about trying more dev work with it.

mellosouls · 2 years ago
It would be interesting to know how long the author has been developing software

It's on the About page:

"I have been programming professionally for more than 30 years"

pc86 · 2 years ago
Overall a pretty good article, however I do feel like the author misses the mark on a couple points.

> Unit testing private methods.

As someone else pointed out this leads to accidental testing. I'm not a test zealot, I think 100% coverage is a fool's errand, and I think TDD is an abomination, but well-structured, well-thought-out tests can be a game changer when used appropriately. Testing things by accident has inevitably lead me to finishing some piece of work then spending half a day or more tracking down why some test failed inexplicably.

> Using an IDE.

I think a better point here is to get really good with whatever tool you use. If you know every incantation in vim you're going to be amazingly productive. If you know every keyboard shortcut in IntelliJ you'll be as effective, but probably not much more. The person who knows vim or emacs in and out will beat the person clicking around in an IDE every day of the week.

That being said some of it is spot on in my admittedly limited experience (only about 13 years or so, in a handful of industries, never FAANG-level scale). The point about commenting problem areas in the domain has really changed my approach to comments. I don't write any comments about what the code is doing unless it's a "here be dragons, don't change $X unless you're free the rest of the week" kind of warnings. But I comment extensively why the business or regulation requires A or B to happen instead of the more straightforward C.

The ChatGPT bit as well matches my experience. For well-defined things where it's hard for ChatGPT to make the answer up, and easy for you to verify if it does (or at least low-damage), it's worth the $20/mo IMO. I tried using it to learn CDK and while I'm not sure it saved me any time, it did save me from having to trawl through AWS documentation.

randomdata · 2 years ago
> I think TDD is an abomination

I'm intrigued. TDD may bring way more pomp and circumstance than it deserves, but when you get right down to it all it says is:

1. Write a test first so that its failure state is proven.

2. Test behaviour, not implementation.

I have certainly been caught in haste writing a test after the fact, messing it up – in a way that saw it pass, and then later realized that it wasn't actually testing anything. #1 solves a real problem.

Which must mean the abomination is the behaviour testing? Do you really care about the implementation, though? Surely that can be considered throwaway? You are not going to gain anything asserting that you used bubble sort and have that assertion fail when you update your code to use quick sort.

pkolaczk · 2 years ago
> 1. Write a test first so that its failure state is proven.

This is useful for to me only when fixing existing code but not when writing new code. Before I write the implementation, it is obvious the test would fail. By not having to code the test first, I have more flexibility on changing the API if needed.

jdlshore · 2 years ago
3. Work in very small steps (a handful of lines of code) and incrementally build up as you go.
nikitaga · 2 years ago
(not the OP, just my two cents)

(1) is the distinguishing characteristic of TDD, but it is not an end in itself, it's a mere way to achieve (2). You write the test after you have the API design, but before you have the implementation, and thus supposedly decouple your test from the implementation, since you don't have the implementation yet.

This approach never sat right with me.

* It makes design iteration more costly: if you want to change the API design as you're implementing the thing, now you have to change a bunch of tests to use the new API, and that's when you don't even have the implementation to test yet! This makes you subconsciously prefer quick and dirty design patches instead of making bigger but better changes to the design. So you're statistically trading potentially better design for potentially better tests. I'd rather have the former than the latter, because it's much more insidious.

* Just like TDD encourages writing tests for behavior instead of implementation, it also encourages relying on tests as the sole arbiter of code correctness. The supposed better quality of tests is offset by over-reliance on them, including by not adding tests to check the weak points of your implementation (because you already wrote all the necessary tests before you came up with it... right?). I've worked with some TDD-loving developers who didn't even feel the need to run the code they wrote on their dev box, in the application context. Tests pass, therefore straight to production. The bugs caused by this attitude offset all other advantages of TDD.

* TDD pretty much relies on 100% unit test coverage, because since you (supposedly) don't think about what the implementation will be, you don't know in which methods the bugs are likely to be. This causes you to over-invest in writing unit tests, but writing and maintaining tests is not free. The risk of bugs is not the same in all of the code, and same for the cost of bugs. Not to mention that some of the code is better tested at a higher level with integration tests or end to end tests, but IME nobody takes TDD there, probably because it's too annoying.

* Overall, TDD seems like an overly elaborate way to proactively "gotcha" yourself with various unhappy-path edge cases before even thinking about the implementation – thinking that would, in fact, expose more of such edge cases. I find that instead of this weird backwards overhead-heavy process, the usual development process works just fine. Start with the big picture, and leave messing with small details for last. Whereas TDD requires you to bring up all the small details / edge cases upfront (otherwise it's indifferent from regular testing).

Like with most ideologies, there are some good high level principles to take away from TDD, but at the end of the day, most ideologies like this are designed to replace unreliable thinking with reliable process. And while that process may indeed reliably produce some kind of consistent output, it's not necessarily better than what you'd have without this process, or with a different process.

0x445442 · 2 years ago
Unit testing private methods via public interfaces is accidental testing and leads to overly complex and brittle test code. The author’s first instincts were correct here.
jcpst · 2 years ago
This perspective is really interesting to me. I’d be interested in knowing what people find problematic with it.

I used to unit test all the components individually. But these days I test with most of the componentry wired up, only mocking the i/o layers. I end up writing fewer tests, they have been less brittle, and refactoring is easier.

0x445442 · 2 years ago
If you want to expose issues in a code base’s architecture, don’t allow mocking frameworks and see how complex and lengthy unit tests become.
SoftTalker · 2 years ago
How is it accidental? The code can only do what its public interfaces expose. This indirectly (not accidentally) exercises the private methods.

If all the public interface test cases pass, isn't that enough?

mvdtnz · 2 years ago
I don't necessarily agree, I think it's situational but I think that the dev community as a group has swung too far to the "only test public interfaces" side lately. It ignores some important realities in favour of ideological purity.

Sometimes there are well-defined processes for performing a task, and that task is performed in only one place in the system. Therefore the details of the process can be kept class private inside of the only consumer. That doesn't mean that the processes should never be tested. If the task is cumbersome to set up or runs slowly then there is good reason to test the internal parts of the process which can be tested with dozens, hundreds or even thousands of permutations of input data cheaply and efficiently. Always relying on the large-scale tests to hit every combination of inputs for a well understood subroutine can be inefficient.

You could make the argument that this well-understood process could be broken out into its own class/package/module and tested with its own public interface, but if there really is only one consumer then that's kind of a strange trade-off to make in many cases.

isityouyesitsme · 2 years ago
Whether or not it is enough depends on the context. Some cases require 100% condition and decision coverage. In these cases, it gets to be very difficult to achieve that unless you are directly testing the functions you write, not through a caller.

But even deeper, it is "accidental" because you are not testing contracts on the public APIs, you're testing the private functions through a layer higher. When the implementation changes, you have coupled your tests so tightly to the implementation that they are useless for regression testing. If not, then you actually aren't testing the private function at all, you're just hitting some parts by coincidence.

0x445442 · 2 years ago
There are a number of issues. First, the fundamental unit under test is the method/function, not the entire call stack. Second, complexity starts to grow when there are code coverage gates in place.

When I see tests that are orders of magnitude larger than the units they’re testing and contain mocked references that are used 7 stack frames deep I know the author had a fundamental misunderstanding of how to write helpful and maintainable tests.

wvenable · 2 years ago
Tests freeze code and design. Often this is exactly what you want but sometimes it's not what you want at all. If your internal interfaces are unit tested then you can't refactor easily because you'll break all your tests. However, if only your external interface is unit tested then those tests actively benefit refactoring the internals.
IshKebab · 2 years ago
I agree. It's also not always possible to sufficiently stimulate a private function using public methods.

He was right to change his mind about the other things.

I was also skeptical about remote working, and while I think it is worse in most ways than office working, it's not a lot worse, and the lack of commute is sooo big a benefit that overall it's totally viable.

waynesonfire · 2 years ago
if your private methods are complicated enough that you're not able to achieve sufficient coverage through public interfaces it's time to refactor. make the private code a library that you can test in isolation.
BeetleB · 2 years ago
This.

When I did C++ programming, I would test via public interfaces. If it didn't seem sufficient, or got messy, the reason would always be "This is a big class that is doing a lot of stuff." I would then identify all the things the class was doing, make classes for each one, and have instances of them in the original class as private members.

This way I could test the individual classes easily, and still test the (formerly) big class using only the public interface (with appropriate mocking as needed).

The main thing I learned from unit testing is to pay little attention to the word unit. Insisting on testing the private methods after the exercise above is usually a symptom of aiming for a level of intellectual purity that does not benefit the code.

(Of course, a better solution may be not to use OO to begin with, but that's a whole other discussion).

isityouyesitsme · 2 years ago
Exactly.

I worked in a place that disallowed friend functions and classes generally, but explicitly for unit testing. I never wrote another private function. Instead, there was an explosion at the tiny-free-function-that-could-have-been-a-private-function factory, and the parts ended up everywhere.

xlii · 2 years ago
IDEs are a deep topic but I don't hold such absolute.

For some languages using commercial IDEs is a very smart choice. Refactoring TypeScipt, for example, with Jetbrains IDE is a breeze, and I think it should be available in developers toolkit whatever their preference is.

For some, more dynamic/niche languages (Hello Elixir!), IDEs stand in a way because they take longer to set up and they still don’t produce results as good as glued together scripts and editor macros (side note: macros aren’t only for inserting text, one can pick text under cursor and search for a specific pattern using rg or even send refresh signal to a browser on the other screen).

There are also two other layers that I always mention as arguments against IDEs.

First is that IDEs change often and no matter how much you try your workflows are going to change. It’s hard to get high proficiency when things are changing and having new IDE feature replacing Your Way is a pain and a learning deterrent (thing that I experienced multiple times).

Second is something I call GPS Development. When I work with IDE I tend to not pay attention where am I and where I should be going because hopping navigation is super easy. And then when I am deprived of those tools I’m completely lost and not productive at all. With arguably dumber tools I can open shell and still navigate and edit with whatever editor I have and it still works well. Thing is that with my line of work stuff breaks often and IDEs rightfully decline to work on broken codebase.

My current stance is to use whatever you’re most comfortable with (on a unit by unit basis). Struggling with editor or IDE, even the coolest/smartest one, is going to interrupt your thinking process and cause performance hit much bigger than whatever gains it could ever produce.

livrem · 2 years ago
In my experience it comes down a lot to what people in a project use. If almost everyone has a properly configured IDE then the code will become almost impossible to navigate without one. If almost everyone uses emacs or vim then there is little risk that the code ends up like that. If you have an IDE it does not matter much how the code is structured as nothing is more than a single click away anyway and you barely notice when clicking if you jumped to somewhere else in the same big file or if you ended up in another file, sometimes in a far away subdirectory. Code structure becomes more obvious and important in my experience when you use a text-editor. For better or worse.
brabel · 2 years ago
> Thing is that with my line of work stuff breaks often and IDEs rightfully decline to work on broken codebase.

So you throw away smarter tools like IDEs because they do not understand your code when it's broken, but then you start using dumber tools that NEVER understand your code?

I would say an IDE can still do anything your dumber tools can (grep, dumb find-replace) even when your code is broken, but when it's fully working it gives you so many superpowers it's incredible you would not want that.

xlii · 2 years ago
The thing is that those dumber tools can do smarter things than IDEs.

I had 2 use cases lately - one was to investigate usage of all function bodies that used specific library for the last 6 months. JetBrains IDE has semantic search but it requires a lot of fiddling (syntax feels very foreign, might be Groovy thing or their own DSL and doesn’t span across multiple commits.

Second was to navigate through a custom pre-transpiration layer, which JetBrains could pick up around 30% of the times.

Those are two cases solved quickly with list refiltering (first one took 15 minutes, second took less than 3). And they were portable, as sharing was a simple copy & paste.

The gist of it is that dumb and smart is marketing thing. ripgrep is dumb tool just as fzf is, but they can be made into a very smart setup.

IDEs can make high general work impact but from my experience suck badly when you enter road less traveled.