6 ms·
I agree with the latter parts of your comment, but not that "What makes unit tested code maintainable has nothing to do with how application code is structured"
by pqh 9y ago
I agree with the latter parts of your comment, but not that "What makes unit tested code maintainable has nothing to do with how application code is structured".
My original point was that code that's hard to test is often hard to refactor.
- curun1r 9y ago> My original point was that code that's hard to test is often hard to refactor I agree with that. The poster I was replying to said somewhat the opposite. He said: > Making that code unit testable would have complicated it, thus lowering its maintainability My basic point was that less complicated doesn't mean maintainable. There's no perfect design in the face of future changes in requirements. So the most important quality of the code we write today is the ability to refactor it and know that it still satisfies all the initial requirements that haven't changed. Unit tests give you that property, lack of complexity doesn't. Simple code can be better than complex, but it's a secondary concern to a comprehensive test suite. I'll take a full test suite over code quality any day because it allows me to go in and add the quality later without fear of breaking things. I guess I misspoke when I said "how the application is structured" since it you've interpreted it differently than I meant it. It was a reference to the line I quoted above...that complicating code lowers it's maintainability. I just meant that complexity and tested are separate concerns and that tests more so than simplicity make code maintainable. So I think we agree more than we disagree.
- loup-vaillant 9y agoWhat I said hinged on a very narrow definition of unit testability, one that I believe is quite useless. My code was highly testable, and thoroughly tested. Those may not have been unit tests™ (I hard coded my A->B->C dependency chain instead of injecting it), but I don't care: it works, it's simple, and it's easy to read. > There's no perfect design in the face of future changes in requirements. So the most important quality of the code we write today is the ability to refactor it and[…] I take issue with the idea of such protean requirements. I'm more the YAGNI type: those changes you're trying to anticipate? They likely won't happen. I'd rather keep the code simple, and add the flexibility only when needed. Simple code is easier to modify anyway. (Of course, I'd rather not ossify an architecture if I can avoid it, but an ossified architecture is often the sign of tight couplings, which to me are a form of complexity. If we keep the interfaces between modules small and simple, we are more likely to end up with something flexible.)
- curun1r 9y ago> I hard coded my A->B->C dependency chain instead of injecting it Read my above point about the cartesian product of tests...if you're doing this, you're almost certainly not getting particularly good coverage of edge cases even if your test coverage percentage is high. If I have test A1, A2, A3 that use a mocked B and B1, B2, B3 that use a mocked C and C1, C2, C3, I've essentially tested 27 different cases that you'd have to write in your hard-coded dependency style while writing only 9 tests. When an application reaches even a moderate amount of complexity and that dependency graph grows beyond 3, that ability for unit tests to cover a geometrically-increasing number of ways that the software behaves is going to be hard to overcome with integration tests like you seem to have been writing. But, again, maybe your dependency graph wasn't big enough to need this. Maybe there was some other mitigating factor that made your situation different. I don't want to speculate on any individual situation since I haven't seen the code and there's no universally correct answer. > I'm more the YAGNI type: those changes you're trying to anticipate? They likely won't happen. The one virtually certain thing about the future is that there will be change. Of course you mostly can't predict what will change, but you can be damn near certain that something will. YAGNI is not about predicting that nothing will ever change, it's only about avoiding guessing at what will. Tests on current functionality are not guessing about what will change. The whole point of a test suite is that it avoids ossification. What can be less rigid than something that can easily be pulled apart and put back together in some other form while being sure that it still works? Simple and flexible without that property of safe reorganization is still a landmine waiting to go off in the face of future change.
- loup-vaillant 9y agoOK, let's get practical. A happened to be a reactor base class inspired by http://ithare.com http://ithare.com ; B was a MWSR concurrent queue (scary mutexes and all) ; C was a ring buffer on which the queue was built. The goal of the whole thing was to pass plain data objects between reactors. The one "requirement" that ended up changing was that plain data objects weren't enough (we want to pass anything that has a move constructor). This means scrapping the ring buffer, and modify the MWSR queue to use something else (possibly an `std::queue`). I used property based tests. With relatively little effort, I ran thousands of tests. Allow me to laugh at your measly 27 from atop my high horse. More seriously, though, I don't know how I could have mocked my ring buffer without writing another full featured ring buffer. Mocking looks a bit ridiculous in this case. Same for the queue. And the reason why this is so is not tight coupling. It's because my queue relied on the functionality provided by the ring buffer, and my reactor relied on the functionality provided by the queue. If this wasn't the case, I would have written a simpler ring buffer and queue to begin with. Also, why mock stuff when I can test my code under real conditions? > The whole point of a test suite is that it avoids ossification. This feels backwards. If you just mean that test suites prevent the unintended consequences of change, sure. They're like a home made type system in that respect. But if you suggest we should bend a code base just to make it "let's mock everything" testable™, then no. Don't mock stuff for the sake of it, it's a wast of time. I'd rather concentrate on making the code usable in the first place. With small, well defined interfaces. In my experience, such designs are naturally testable.