9 ms·
How to write untestable code (2008)
- dmitriid 5y agoHow to write untestable code? Use any of Google's cloud libraries. (Well, not just Google's). Good luck testing your cloud functions, dataflows, BigQuery integrations etc. etc. etc.
- Jtsummers 5y agoNovember 5, 2009 (29 comments) https://news.ycombinator.com/item?id=924859 https://news.ycombinator.com/item?id=924859 I thought I'd seen this here more recently, but I guess it was another similar article.
- rmorey 5y agoThis is really funny
- charcircuit 5y agoI disagree with >Create Utility Classes and Functions/Methods This doesn't make something untestable. Take for example a max function. A max function such as Math.max is a part of a utility class, but it's easy to test.
- mertd 5y agoI believe they mean utility classes which would need explicit instantiation and lifetime management to run any test that might call them at some point like: MathOperationsProvider::Init(endianness_config, locale, wolfram_alpha_connection).
- drewcoo 5y agoIf you can't justify it. And keep justifying it every time someone asks. Then don't do it. Max . . . max is pretty easy to justify. But MaxMinusMagicFiveForUnseenReasons . . . not so much. The point of all of this (I assume TotT) is to have the conversations. If you're not having them at design time or code review time or incident response postmortem times, you're doing it wrong. And you're reminded every time you visit the restroom (again, if it's TotT). That's my interpretation.
- charcircuit 5y agoThat is more of a maintainability problem than a testability problem.
- deleted 5y ago[deleted]
- deleted 5y ago[deleted]
- rubyist5eva 5y agoThe number 1 way to write untestable code - is to build your entire project without writing tests at all. Adding tests after the fact will make you want to kill yourself, whereas when you write tests either before or at the time of writing you will generally take the path of least resistance for writing code that can pass those tests.
- postalrat 5y agoHow much refactoring do you tend to do at the start of a project and do your tests help refactoring?
- jmmv 5y agoNot the parent’s author, but I tend to write extensive tests from the very beginning. Sometimes they are indeed annoying while refactoring, but more often than not they are very helpful in ensuring that the whole thing still works. And if the tests are annoying that’s usually a sign that they can be improved.
- rubyist5eva 5y agoPersonally, I don't do much refactoring. If I'm exploring a new idea, I write tests after I've experimented a bit and have a good idea of what I'm going to build - and then write mostly integration tests with very few mocks (usually only calls to external services). I don't refactor until I come back to add features, and yes, the tests help immensly. Red->Green->Keep Moving
- postalrat 5y agoHow much experimenting do you do? You could take that as far as writing it once without tests (experimenting). Then write test for what you created. Then start over and keep the tests.
- drewcoo 5y agoWith that kind of take, I'll raise with "write untestable code by writing for only you and only current you." I'm often surprised how thoughtful past me was with code, knowing how people might try to screw it up. Even to the point of having useful and sometimes fun error messages. Feedback from other people agrees with that assessment. Avoiding misuse is avoiding the untestable.
- elboru 5y ago> Depend on Concrete Classes - Tie things down to concrete classes - avoid interfaces wherever possible. Well, yes, if those classes are implementation details why would you want otherwise? Just to write “unit” tests with mocks all over the place that later will be broken after the first refactor?
- Jtsummers 5y agoOne of the premises/promises of OO is the ability to substitute implementations (in particular, dynamically, but in a modular programming sense also statically). An over-reliance on concrete classes makes this harder to achieve, and (in the context of the list, which is about testing) means you have a larger portion of the program that has to be brought into the test harness to test something. On the former, using concrete classes instead of interfaces can be worked around if they're still configurable, but now you have inappropriate (or potentially inappropriate) subclasses to compensate for the lack of an interface. On the latter, well, it makes testing harder which is the entire point of the list. If A has two components B and C which are both tied to concrete classes and not interfaces, then you have to bring A, B, and C into the test harness when you really just want A and (possibly) B or C or stubs/mocks for them. If you don't care about testing (and, in particular, unit testing), and don't care about substitutability, then don't worry about it.
- deleted 5y ago[deleted]
- deleted 5y ago[deleted]
- deleted 5y ago[deleted]
- elboru 5y agoI care about testing and that’s why I hate to break my tests every time a refactor comes. If the behavior doesn’t change my tests shouldn’t break. Tests should meant to be there to give me confidence about the main functionality. They should tell me when my main functionality is broken or not after a change. It’s not very useful to me if I get red tests just because the structure of my code changed. Those red tests don’t tell me if the functionality is the same or not. If my tests break every time even if the functionality is the same, then I start to ignore the warning, I just blindly make them pass. They become “The Boy Who Cried Wolf”.
- mrblampo 5y agoInteresting how OO-focused this is. I work at Google today, in relatively old C++ and JS (now converted to TS) codebases, and we have no particular emphasis on forcing every function to be a method of a class. I don't think this has especially harmed testability.
- drewcoo 5y agoWhat part of OO was about testability? Single responsibility? Liskov substitution? Interfaces to test instead of implementations? Just chunking things into bite-sized pieces? I honestly don't get it.
- henning 5y agoThese seem like a great way to write simple, high-performance code that is easy to write (because you just think about your actual problem vs. what someone who wants to ignore the problem wants -- what arbitrary slice are they expecting your code to cover?) and easily tested with simple integration tests that use quality fixtures which exercise your code thoroughly. Endless classes of bugs eliminated through these "anti-patterns". Guess what: if unit tests pass in an OO system, it doesn't mean a fucking thing, especially dynamic languages where simply changing a name in a large codebase is a risky operation. And fuck virtual methods.
- shoo 5y agoif i could only afford a single test, i'd much prefer it to be an integration test rather than a unit test. but, some kinds of code can permit a huge number of execution paths with different behaviour and interactions that need to be tested. If you want to cover all the different paths using only integration tests without decomposing a system into simpler subsystems that can be tested more locally, then you end up with a combinatorial explosion in the total number of execution paths and the number of test cases you have to write and maintain.
- boyter 5y agoHah! I actually took this sort of joke further than I probably should have a while ago with this https://boyter.org/posts/expert-excuses-for-not-writing-unit-tests/ https://boyter.org/posts/expert-excuses-for-not-writing-unit... It also covers how to avoid writing them to begin with.
- revskill 5y agoFollowing Pareto principles: 80% of tests should be convered with good static typed language and a clean, layered structure. Only 20% left is to focus on testing business logic.
- drewcoo 5y agoIf most testable paths are covered by typing, then what? You're working on some kind of filter app? Sure, then, probably more than 80% of problems are caught with type checking. I'm not saying that's not coding, because lots of things are coding. But I am saying that's not the usual case that I see.
- formerly_proven 5y ago> Following Pareto principles: 80% of tests should be convered with good static typed language and a clean, layered structure. My experience in dynamically typed language is that it's very rare to see a typing error that doesn't co-habitate with at least one logic error.
- shoo 5y agoWhat's the least testable code you've had the pleasure of dealing with? From my first commercial software job: a few thousand lines of complex C++ (mix of decoding the output of a mathematical model mixed with business rules) structured as a single "god class" with heaps of internal state and a dozen methods -- comically all of the methods would return void and took no arguments, and would call each other. It took days of careful refactoring and consultations with a colleague until it was possible to instantiate one of the things in a test harness and do nothing with it. In some sense this was dead simple code as there were no dependencies on external resources or concurrency.
- dwohnitmok 5y ago> OO cowboys will want to have a whole polymorphic soup of collaborating objects. Say no to the OO-ist. When you nest and branch conditionals, all you need to do is read the code from top to bottom. Like a great novel, one simple linear prose of code. With the OO-overboard paradigm, it's like a terrible choose-your-own-adventure kid's book. You're constantly flipping between classes and juggling patterns and so many more complex concepts. Just if-things out and you'll be fine. This is meant sarcastically, but I think this is actually one example of where a lot of backlash against OO comes from, especially in 2022.
- jiggawatts 5y agoI come from the opposite camp. I detest functions that are five pages of boolean logic, which I guarantee you contain at least a dozen bugs, no matter how much you think you've tested it. Code coverage is like those skin creams that say "Scientifically tested!" on the label. Yeah, well, tested doesn't mean that it worked. It just means that there was a test. Watch this video of Andrei Alexandrescu's talk in CppCon 2015: https://www.youtube.com/watch?v=LIb3L4vKZ7U https://www.youtube.com/watch?v=LIb3L4vKZ7U I've done things like this in my career, and it's always worked out brilliantly. Bugs just evaporate when you let the compiler "combine" things for you, instead of just plopping everything into a bowl and mashing it together with your hands like some sort of animal. Especially in today's C++ or Rust, you can "have your cake and eat it too". It's possible to build up these nice, single-purpose class hierarchies and combine them into complex, powerful constructs via templates / traits. The compiler takes care of the edge cases for you, because it never gets bored or distracted. There's no performance downside because of static dispatch, inlining, and aggressive optimisers.
- rmbyrro 5y agoBut having a function spanning five pages isn't smart practice, regardless of OOP or Functional style.
- marcosdumay 5y agoWell, usually the way to avoid a 5 pages function is by encapsulating concepts away into self-contained code. It is not by encapsulating it away into code colocated with state. OOP is not the answer here. Class hierarchies can be, as long as they are not OOP and "classes" are just synonym for "modules". Code and data colocation has its uses, but it's normally best fit for interface layers than internal code.
- vrnvu 5y agoIt's ironic what attempted to be "satiric" and a writing on "good tips" back in 2008 has become a list of red flags in 2022.
- jackblemming 5y agoYep. Static utility classes with pure functions are a good thing. Not everything needs to be an instance or an object. Not every if or switch needs to use polymorphism instead. Still lots of good tips here. Can anyone name a Google library they've actually enjoyed using? Every library I've greatly enjoyed has come out of Facebook, never Google.
- capableweb 5y ago> Can anyone name a Google library they've actually enjoyed using? No, I'm mostly with you here. Closure Library is OK if you don't have anything else that replaces it (or if something you use ships with it, hello ClojureScript) but other than that, their libraries leave a lot to offer. But Google Closure Compiler is magic (in a good way) though, but you can't call that a library. > Every library I've greatly enjoyed has come out of Facebook, never Google. I'm sure I'm reading this the wrong way, because surely you can't mean that every library you've greatly enjoyed has only came from Facebook? Most of the really well made libraries I've used in my time, has come from private people, besides React. But now React has better replacements available as well, like Preact. But other than that, larger companies seem to be really bad at writing libraries for the world at large, which makes sense, they mostly care about their own use-cases.
- disgruntledphd2 5y agoI'm pretty sure that they meant that FB tends to write more useful libraries than Google.
- logicchains 5y agoThe Tensorflow C++ api seems like it was written by someone whose dog was raped and murdered by the author of a Modern C++ book, so they vowed to only code in '90s Java style and make maximal use of the C preprocessor.
- Toine 5y agoI completely disagree with the general sentiment here, this is still very relevant today, misunderstood, and much needed in many codebases.
- steerablesafe 5y ago> C++ specific: Use non-virtual methods Are they suggesting that all/most of my methods should be virtual? That's just nuts. Next they will say that all of the members should be public, since it's easier to test that way.
- wheybags 5y agoI have unironically used `#define private public` before including headers in my test code...
- steerablesafe 5y agoWell, I hope you know that in theory it's undefined behavior, and in practice it can actually affect layout of objects.
- michalhosna 5y agoWhy is it undefined behavior?
- steerablesafe 5y agoIt looks like it's a C++ standard library requirement: https://eel.is/c++draft/macro.names#2 https://eel.is/c++draft/macro.names#2 Otherwise this is one of the unintended effects (on Itanium ABI): https://godbolt.org/z/bY4534zhc https://godbolt.org/z/bY4534zhc
- alternatetwo 5y agoFYI, MSVC doesn't do this. I use this same hack in a project where I need access to private/protected members in the current stage, and memory layouts need to match ... You gave me a bit of a heart attack there. mingw gcc (11.2.0) does have the same 8 vs 12 difference.
- 5y ago
- sirmarksalot 5y agoI was a huge fan of this article and Misko's accompanying talks back in 2012 or so, but a lot of this hasn't aged that well. These ideas all assume that in all cases, the class should be the unit under test, and you can see how this plays out in Angular, where the command-line tool will generate stub unit tests for every new component you create. If you fill out those tests with the aim of maximizing code coverage, then what you'll find is that all of those tests will need to be rewritten at the first refactor. On a personal project, that might be an enjoyable way to spend a bit of time, getting to understand your project better, but on a team with actual deadlines, there's not going to be enough time to do that. Either you remove the tests entirely, or you put off the refactor indefinitely. Class-based unit tests lock you into a specific implementation, providing a checksum on the current behavior, making it as hard as possible to fix an actual bug when it's discovered. Collaborators-included unit tests on the other hand, do not break unless your product is broken. It can be harder to chase code coverage when you're doing laparoscopic surgery on your code, but the end result is that every conditional has an actual use case behind it, and if the requirements change, you can see the name of the test with the bad requirement and delete it. I still think that a lot of the patterns that make your code testable are still good though. It's not always clear at what level of abstraction you want to start mocking. Do you mock HTTP? Do you mock a martialed API? Do you mock a business logic abstraction that depends on that API? If you use DI for all of these layers, then you don't have to get it right the first time. You have options when you're writing your tests, and can use concrete or mock collaborators according to whatever your current need is.
- jve 5y ago> Be Defensive - They're out to Get Your Code! - Defensively assert about the state of parameters passed in methods, constructors, and mid-method. If someone can pass in a null, you've left your guard down. You see, there are testing freaks out there that like to instantiate your object, or call a method under test and pass in nulls! Be aggressive in preventing this: rule your code with an iron fist! (And remember: it's not paranoia if they really are out to get you.) This seems nuts to me? Sacrificing guarantees for the sake of testability? I mean defensive coding is one of good tools to fail early and with meaningful error messages and confidence on state within your method. Moreover there are tons of points just to enable mocks'n'stuff. Why not just go higher up the abstraction layer and test things functionally there? It's more useful IMHO than some state object that may never be true in real life. It reads like chasing some code coverage metric with fake state. When in reality, you may still get nullreferenceexception because your assumption on false state was incorrect. Or some method call there may fail with unhanled error code or whatever. It doesn't take branches for fully covered code to fail. IMHO unittests are useful, but in limited scope.
- dgb23 5y agoIt seems like you need comprehensive unit tests if your implementations are shallow and grant few guarantees. I like TDD-style example tests to create a fast feedback loop during initial development or refactoring though.
- marginalia_nu 5y agoIn my experience, if you find you need to check every parameter because your code keeps blowing up from all the bad parameters its getting, the problem is at the source of the bad data. Checking input everywhere is a questionable treatment of the symptom, it won't fix the bigger problem that some part of your code is producing invalid output. It's a lot harder to fix bad data as a consumer of that data, than as its producer. This primarily applies to internal code. For external APIs there is no such thing as too much validation.
- jve 5y agoI don't trust my future self that I'll start feeding in bad data. Of course there are some boundaries, but not too deep. I've started more of defensive coding when I've read that for a software I forgot how it called - there was security audit which find no important security flaws. Because their ideology was to not trust the input, even if its your input. Today your assumptions are correct, tomorrow, because of changes, they can be incorrect. I'll post link here if I find it. I think I read it on HN few years ago. Edit: A-ha, here it is. Dovecot. HN Article: https://news.ycombinator.com/item?id=13407233 https://news.ycombinator.com/item?id=13407233 and https://wiki.mozilla.org/MOSS/Secure_Open_Source/Completed#dovecot https://wiki.mozilla.org/MOSS/Secure_Open_Source/Completed#d... > The Cure53 team were extremely impressed with the quality of the dovecot code. They wrote: "Despite much effort and thoroughly all-encompassing approach, the Cure53 testers only managed to assert the excellent security-standing of Dovecot. More specifically, only three minor security issues have been found in the codebase, thus translating to an exceptionally good outcome for Dovecot, and a true testament to the fact that keeping security promises is at the core of the Dovecot development and operations." Practices documented in source: https://github.com/dovecot/core/blob/master/doc/securecoding.txt#L13 https://github.com/dovecot/core/blob/master/doc/securecoding... > Don't rely on input validation. Maybe you missed something. Maybe someone calls your function somewhere else where you didn't originally intend it. Maybe someone makes the input validation less restrictive for some reason. Point is, it's not an excuse to cause a security hole just because input wasn't what you expected it to be. Perhaps it's just my way of dealing with "I know that input is validated". Yeah, defensive coding can get verbose, but makes me feel confident. + The NASA Coding Guidelines of states: > Each calling function must check the return value of nonvoid functions, and each called function must check the validity of all parameters provided by the caller. > The code's assertion density should average to minimally two assertions per function. /just extracting useful bits of advices from veterans
- axiosgunnar 5y agoThis can be summarized to: How to write untestable code: Use OOP ;-)
- wwilim 5y agoThis article actually has educational value other than laughs. Counter-examples are sometimes easier to remember than straight examples, especially if you're reading about pitfalls - your brain will match those patterns when it sees you're writing them. A text that explains what the wrong way looks like and mocks it, in my opinion, will be more effective than a text that explains the right way in great detail.
- Supermancho 5y ago> When you use static methods, they can't be mocked using mocking libraries Except they can, in most ecosystems.
- kazinator 5y agoDefensive programming is important. It's better to get an assertion at the API level about a null than an error message that + cannot add 42 and null.
- peterhil 5y agoI would add: * Throw useless errors on getter or has methods! I recently gave up on using MapboxGL.js because of this.