News for hackers. Stuff that matters.
Re:How to avoid brittle code by duncanawoods on Monday March 28, 2016 @03:53AM (#11373183)
This response has been trotted out a few times on HN. IMHO its terribly mistaken.

An analogy is that unit tests are like a brick layer's spirit-level and plumb-line. As you build the wall, you ensure the pieces are put together correctly and head off mistakes early.

The "only test at a high-level" is like saying don't confirm the bricks you are laying are correct, instead bring in an inspector to check the whole building is up to code once we have finished.

If we do bring in the inspector she takes one look and says the wall have to come down because of faults right at the bottom or because we forgot the damp course or we never get that far because we couldn't fit the roof because all the walls bowed out.

I fear that those who advance anti-unit-test views only do so because they lack experience with large complex code-bases and that they are not in a position to be advising others.

Re:How to avoid brittle code by pka on Monday March 28, 2016 @04:09AM (#11373212)
The situation you are describing is best handled by generative tests (i.e. Quickcheck).

And this is basically the only time I would write an unit test - when I can not capture most invariants in the types. Everything else is basically double the work and a waste of time.

Integration tests are always a good idea, but hard to get "conceptually" right. When you get to the level of testing multiple components through the whole stack most of the time IO is involved and this gets hairy fast, not to mention that one mostly tests the 3 codepaths they can come up with.

Maybe a DSL/swappable interpreter approach is one way do that? Or make everything pure again with FRP?

Re:How to avoid brittle code by duncanawoods on Monday March 28, 2016 @04:21AM (#11373232)
I'm not a Haskell programmer but how would you deal with a case like e.g. using a set of complex regexes for parsing some unusual date-time formats. I would want to unit test the hell out of them covering all the variations and exceptions I could think of.

I can't see how exhaustively testing a small unit like this with high-level tests would be practical in terms of setup effort and run-time.

Re:How to avoid brittle code by pka on Monday March 28, 2016 @05:00AM (#11373311)
In this particular case I would use some Quickcheck-like generative test library and give it a description of how to generate random dates (i.e. start by generating a day, then a month, then a combination of both etc) and then leave to it the generation of all test data.

The amount of edge cases Quickcheck catches is beyond the most comprehensove unit test suite.

On a higher level though I would use parser combinators. If you have a grammar specification you can then directly transform it into a parser and thus would only need to test that you transformed it correctly (i.e. prove the proof as opposed to prove the implementation if that makes sense).

Basically - try to capture most of your invariants in the types and then you know your code is correct. The invariants may be wrong though, in which case integration testing may help.

However, I still have no idea how to test programs with complex state and IO in a way that isn't basically "script the 5 behaviors I can come up with and pray for the best", in which case I mostly don't even bother (i.e. UI etc...)

(I know this is a very short answer but I'm typing on a phone)

Re:How to avoid brittle code by mattnewton on Monday March 28, 2016 @11:34AM (#11374854)
You can make a finite state machine to model your expectations on random input, but then your test code is almost as complex as the code :) I've found quickcheck super useful as a way to fuzz user input or find edge cases in unit tests I didn't see, but I still write all my integration tests by hand because they are so hard to formalize.
Re:How to avoid brittle code by crdoconnor on Monday March 28, 2016 @08:57AM (#11373950)
>I fear that those who advance anti-unit-test views only do so because they lack experience with large complex code-bases

That's funny. I suspect that people who advance the pro-unit test view do so haven't had sufficient experience with large complex code bases.

Experience with large complex code bases with a spaghetti problem is the exact thing that put me off unit tests.

Unit tests are by their nature very tightly coupled to the underlying implementation, which is why they cause so many problems.

It's kind of like pouring concrete over a bunch of code bricks: if those bricks are in the wrong place and need refactoring, by pouring unit tests over them means you're solidifying the broken architecture and making it even harder to fix.

IME there basically don't exist large projects without technical debt problems either, so arguing that unit tests work well when the architecture is good is kind of a moot point.

Re:How to avoid brittle code by AnimalMuppet on Monday March 28, 2016 @03:19PM (#11376607)
First, if you write the unit tests as you're writing the code, they kind of push you to not write spaghetti, because it's hard to test. That doesn't help you if you are adding tests to an existing project that's a mess, though.

> Unit tests are by their nature very tightly coupled to the underlying implementation

No, not "by their nature". Unit tests are supposed to test that a "unit" has the intended external behavior. It shouldn't care about the internal behavior of the "unit". That's the theory. They're only supposed to be coupled to the interface, not to the implementation.

Re:How to avoid brittle code by crdoconnor on Monday March 28, 2016 @05:22PM (#11377509)
>First, if you write the unit tests as you're writing the code, they kind of push you to not write spaghetti, because it's hard to test.

I've found this to be one of the biggest myths of unit test driven development. Developers who have a tendency to write spaghetti do so anyhow. When it becomes too hard to write tests they simply don't write tests or they write 'pretend' tests.

It's perfectly possible to write spaghetti code and write a badly written test that notionally covers it.

Good coders code well. Bad coders don't. TDD doesn't help here and if there is any relationship between TDD and good design, I'm certain it's correlation not causation.

>That doesn't help you if you are adding tests to an existing project that's a mess, though.

Harms you deeply, in fact.

>No, not "by their nature". Unit tests are supposed to test that a "unit" has the intended external behavior.

i.e. it couples itself to a unit, which is an underlying implementation detail.

If it's coupling itself to an externally facing interface it's an integration test or an end to end test.

>It shouldn't care about the internal behavior of the "unit".

No, but then again, since "units" are supposed to be the smallest blocks of code that comprise your code base, it's caring very much about how all of the blocks talk to one another, which is an implementation detail (for your project). Perhaps the most crucial implementation detail, in fact.

How those units talk to one another (their API contracts) is basically the most critical aspect of your project's architecture and the single biggest source of unavoidable technical debt.

Pouring test cement in those cracks means that any technical debt accrued in those API contracts becomes impossible to refactor without breaking the tests.

In practice this makes that refactoring more difficult than if it had no tests at all due to the massive psychological inhibition against deleting tests or turning the build red and the added problem that real failures look the same as changed API contracts.

Re:How to avoid brittle code by AnimalMuppet on Monday March 28, 2016 @07:00PM (#11378158)
> It's perfectly possible to write spaghetti code and write a badly written test that notionally covers it.

Absolutely.

> Good coders code well. Bad coders don't.

Also true.

> TDD doesn't help here and if there is any relationship between TDD and good design, I'm certain it's correlation not causation.

I've used TDD and not used it. And when programming using TDD, my code was less "spaghetti" - more modular, better-designed interfaces. There definitely was causation, in my case - it made me write my non-test code differently. So that's anecdote and not data, but holding the programmer constant and changing the methodology, the style and quality of the code changed.

> >No, not "by their nature". Unit tests are supposed to test that a "unit" has the intended external behavior.

> i.e. it couples itself to a unit, which is an underlying implementation detail.

> If it's coupling itself to an externally facing interface it's an integration test or an end to end test.

Let's get specific, so that our terms can be better defined, and we can make sure that we're not just arguing about definitions of terms. Instead of "unit", let's say "C++ class". Now: By my definitions, my unit test should test the behavior of the public API of that class, not the private implementation. So the API is an implementation detail of the project, but not of the class. I will agree that the unit test definitely couples to the interface of the class. Done right, though, it doesn't couple to the implementation of the class.

> How those units talk to one another (their API contracts) is basically the most critical aspect of your project's architecture and the single biggest source of unavoidable technical debt.

A bigger source of technical debt than the bugs you can't find that are buried in the implementation of some class? I doubt it. And good tests mean that those bugs don't make it very far before getting exposed.

A bigger source of technical debt than that API that you don't know how to use, and so you call it wrong? Tests can actually serve as documentation for how to use an API. You have non-test documentation? You're lucky to have that, but the problem is, when the code implementing the class changes, the documentation might well not. If that happens with the tests, you find out the first time you run them.

Re:How to avoid brittle code by crdoconnor on Tuesday March 29, 2016 @05:46AM (#11380434)
>Let's get specific, so that our terms can be better defined, and we can make sure that we're not just arguing about definitions of terms. Instead of "unit", let's say "C++ class". Now: By my definitions, my unit test should test the behavior of the public API of that class, not the private implementation. So the API is an implementation detail of the project, but not of the class. I will agree that the unit test definitely couples to the interface of the class. Done right, though, it doesn't couple to the implementation of the class.

In almost all cases (the exception being if the class is the core of a library's externally facing API), the class itself is an implementation detail.

>A bigger source of technical debt than the bugs you can't find that are buried in the implementation of some class?

Yes, obviously. Bugs are not technical debt.

>And good tests mean that those bugs don't make it very far before getting exposed.

And integration tests are generally better at finding said bugs than unit tests since they test more realistically.

>A bigger source of technical debt than that API that you don't know how to use, and so you call it wrong?

Is again, a bug - not technical debt.

Unit tests are horrible for this since they require you to mock APIs that you're calling. The test which mocks the API wrongly won't catch that bug but the integration test that uses the real API will.

>Tests can actually serve as documentation for how to use an API.

Yes they can. Integration tests can double as a specification for an entire application which is even better than a documentation for a (potentially) badly architected API.

Re:How to avoid brittle code by mehwoot on Monday March 28, 2016 @05:23AM (#11373359)
and head off mistakes early.

What? On the codebase I work on, integration tests run every single commit, just like unit tests. They would both fail at the same time. Except the integration tests are much less brittle.

Re:How to avoid brittle code by glenjamin on Monday March 28, 2016 @05:45AM (#11373404)
My unit test suite runs in ~500ms. I run it every single time I save a file. In fact, it's set up with a filesystem watcher to do this automatically and announce the result to me via growl.

The faster your test suite, the more often you can run it, and the faster you get feedback on your code.

You can combine this with a healthy integration test suite, and only run the unit tests.

Validation rules is a good example, if I want to test that my user input validation rules are correct, I don't need to do this through the entire application frontend - I can just check those rules in isolation.

Re:How to avoid brittle code by crdoconnor on Monday March 28, 2016 @09:02AM (#11373976)
>The faster your test suite, the more often you can run it, and the faster you get feedback on your code.

In all likelihood, the faster it is, the more tightly coupled it will be. You're simply exchanging computing power for brain power (refactoring tests when you refactor code), which is nearly always a bad trade off.

Re:How to avoid brittle code by glenjamin on Tuesday March 29, 2016 @07:48AM (#11380790)
Usually my unit tests are tightly coupled to the module they're testing, but the modules themselves are loosely coupled from the rest of the system.

In most cases I want to preserve existing behaviour, so I don't want any existing tests to fail.

I'll add tests for the new behaviour in the relevant modules, add code to make them pass, and then if necessary update the glue code / integration tests which cross the boundary.

Pretty much by definition, a refactor of code should not change its behaviour. My tests usually assert on behaviour observed at the boundaries of modules, so they should still pass during refactorings.

Re:How to avoid brittle code by dllthomas on Friday April 01, 2016 @12:53PM (#11406357)
> Pretty much by definition, a refactor of code should not change its behaviour.

Quite precisely by definition, perhaps for other reasons as well :-P

> My tests usually assert on behaviour observed at the boundaries of modules, so they should still pass during refactorings.

I find the kind of refactoring that most desperately needs the support of tests is changing the boundaries of the individual units. Unit tests can still arguably be useful there in making me clarify the changes I'm making to my interfaces... but they do need changing, and I can't be quite as confident that nothing is breaking if I'm changing both code and tests in tandem.

Re:How to avoid brittle code by mattnewton on Monday March 28, 2016 @11:32AM (#11374844)
This sounds hellish, I don't want red things flashing in my terminal while I'm in the middle of changing some function signatures; I know it's broken, I haven't gotten around to writing all the boilerplate to convince the tests I know about it yet though.

I'm with GP about integration tests.

Re:How to avoid brittle code by pekk on Monday March 28, 2016 @02:26PM (#11376210)
Growl doesn't flash red things in the terminal and nobody says you have to use Growl notifications if you don't like them, but this really has nothing to do with the usefulness of unit testing.
Re:How to avoid brittle code by glenjamin on Tuesday March 29, 2016 @07:44AM (#11380772)
Sometimes I'll hit save to confirm that the tests I expect to fail are doing so.

Sometimes I'll wait until I've finished changing all those function signatures until I hit save.

Re:How to avoid brittle code by quanticle on Monday March 28, 2016 @05:35AM (#11373382)
Yes, but I can run unit tests while I'm coding. Waiting for a check-in is far too late. If I've broken something, it's more helpful to let me know about it ten seconds later, rather than hours later, when I've pushed my change and broken the build.
Re:How to avoid brittle code by dllthomas on Friday April 01, 2016 @11:38AM (#11405601)
Some integration tests can perfectly well be run on similar terms.