8 ms·
I tend to mentally divide code into roughly two types: "computational" and "plumbing". Computational code handles your business logic. This is usually in the m
by chimprich 6y ago
I tend to mentally divide code into roughly two types: "computational" and "plumbing".
Computational code handles your business logic. This is usually in the minority in a typical codebase. What it does is quite well defined and usually benefits a lot from unit tests ("is this doing what we intended"). Happily, it changes less often than plumbing code, so unit tests tend to stay valuable and need little modification.
Plumbing code is everything else, and mainly involves moving information from place to place. This includes database access, moving data between components, conveying information from the end user, and so on. Unit tests here are next to useless because a) you'd have to mock everything out b) this type of code seems to change frequently and c) it has a less clearly defined behaviour.
What you really want to test with plumbing code is "does it work", which is handled by integration and system tests.
- davidcorbin 6y agoThis. Making the distinction between the two is huge. Save time and money testing the right pieces of a codebase.
- knubie 6y agoI've seen this concept called by many names including CQS[0] and Functional Core, Imperative Shell[1]. I'm just leaving this comment here for those that are interested in reading more. [0] https://en.wikipedia.org/wiki/Command%E2%80%93query_separation https://en.wikipedia.org/wiki/Command%E2%80%93query_separati... [1] https://www.destroyallsoftware.com/screencasts/catalog/functional-core-imperative-shell https://www.destroyallsoftware.com/screencasts/catalog/funct...
- pydry 6y agoFunctional / imperative doesn't exactly map on to these two concepts. "Computational" is often imperative and integration code isnt always imperative (a lot of react code would fit in this box, for instance).
- eropple 6y ago"Computational" code that isn't by the vernacular definition of "functional"--and you can write functional code regardless of programming language--is something of a red flag to me. Operate only on your inputs. Return all of your outputs. No side effects.
- adamkl 6y agoI agree, but if you watch the linked screen cast you can see that its just Gary Bernhardt's take on Hexagonal/Clean/Onion Architecture. The idea that your business logic should be isolated from external dependencies (in his case, by making the code (pure) functional). That makes it easy to unit test the business logic, and your integration tests should be minimal (basically testing a single path to make sure everything is talking to each other).
- pydry 6y agoThis is adapting the structure of your codebase to the capabilities of unit tests. It has advantages but it's an expensive waste of resources if you have cheap, effective integrarion tests.
- adamkl 6y agoCheap integration tests is usually an oxymoron (when cheap refers to the tightness of the developer feedback loop). Gary was coming from the land of Ruby-on-rails where a full set of integration tests could take hours. In that environment, structuring your code to enable easy testing of complex logic makes a lot of sense. Likewise in a large enterprise environment, where integration testing across a (usually messy) set of interconnected dependencies is a pipe dream. It's true that over-architecting is something to be wary of, but as usual, there's no one-size-fits-all answer.
- greymalik 6y agoHumble Object[0] is another version of this. [0] http://xunitpatterns.com/Humble%20Object.html http://xunitpatterns.com/Humble%20Object.html
- mrloba 6y agoYes! I do something similar which is sometimes referred to as functional core imperative shell. My goal is to put as much code as possible in the computational/functional part. This part is easy to test since it's pure. The remaining plumbing/imperative part has much less code, less dependencies, and less logic, which as you say doesn't need unit tests anymore. It needs less dependency injection as well, which is a huge bonus.
- tsimionescu 6y agoYou should still be careful that your pure logic is actually doing something by itself, rather than just massaging data from one external format to another external format. A lot of code can be in this are where it is absolutely unit testable, but the unit tests are almost entirely useless, as the code only ever changes because the input or output types change, so the tests also need to change. I think of this in terms of code that is 'authoritative' for its logic or not. For example, a sorting method is authoritative - it is the ultimate definition of what sorting means. Also, a piece of code that validates some business rule defined in a document is the authority for that business rule. But a piece of code that takes input from the user and passes it to some other piece of code is not authoritative for this transformation. The functionality of this kind of code is not defined by some spec, but by 'whatever the other piece of code wants to receive', which may be arbitrarily hard to define. Depending on the complexity of the transformation, there may still be reasons to test parts of this code, at least to ensure that a new field here doesn't affect the way we transform that other field there, but often only small pieces of it are actually worth testing.
- tarkin2 6y agoWonderful description. In brief: Unit test algorithmic code; use integration tests for everything else, i.e plumbing code.
- pydry 6y agoThere's a very controversial conclusion to this : some projects ought to have zero or close to zero unit tests. I agree very strongly with this but a lot of people will be very unhappy with this idea.
- collyw 6y agoI agree with this, I also admit that 95% of the code I write is plumbing code. (There is an art to making your plumbing nice).
- marcosdumay 6y agoThe people that place a controversy on this can be safely ignored.
- eweise 6y agoYes I agree as well. Our company uses Spring to write banking software and there is rarely a case that involve purely logic that can be separated from its dependencies. I used to try isolating code into separate methods that took no dependencies but it just made the code harder to read. Now we just test invoking the grpc endpoints and include the db (with rollback) and it works quite well.
- abernard1 6y agoI would suggest making the business logic stateless methods ("functions") that take data records that are immutable passed between it. That allows strict separation of all I/O from testable business logic. If you can't separate pure logic from your I/O, it means you have a Russian-doll program that looks like: readFromApi { doSomeBusinessLogic{ writeToPersistence{ ... Instead of a pipeline like: a <- readFromApi b <- doBusinessLogic(a) c <- writeToPersistence(b) If you do things this way, you can always isolate your business logic from your dependencies.
- jfengel 6y agoThis has been a problem for me with my quarantine project. It's little more than a CRUD app: get some data, download it, display it on the screen. There's practically no business logic to it; the entire project is wiring up various XaaS. By the time I mocked everything that needed to be mocked, I'd have put more effort into mocking than the project itself. I test the parts that are actually mine as best I can, but most of my debugging consists of driving it by hand.
- toast0 6y ago> By the time I mocked everything that needed to be mocked, I'd have put more effort into mocking than the project itself. More importantly, that your app works with the mocks doesn't give you good information about weather your app works with the actual services.
- qmmmur 6y agoWhat if you write code that isn't for a business? How does your workflow apply then?
- jugg1es 6y agobusiness logic does not mean it has to be for a business. It's more like calling the pointy part of a spear the "business end". It's the part that does the job.
- deleted 6y ago[deleted]
- hoorayimhelping 6y agoBusiness Logic is a euphemism. It doesn't mean literally business logic, it means the 'core functionality of your code.' When you design software, you typically model some real world process or system in the abstract. Business logic is the core problem of your model. You can also call it model code, or core functionality. It all means the same thing - it's the important part of your app. Using the old Asteroids arcade game [1] as an example: The business logic is how many lives the player has, what happens when you shoot asteroids (they break up, or disintegrate if they're small), what happens when you reach the edge of the map (you wrap around the other side), what kind of control scheme there is (there's momentum in asteroids, you don't stop on a dime) etc. 1) https://www.youtube.com/watch?v=WYSupJ5r2zo https://www.youtube.com/watch?v=WYSupJ5r2zo
- SamuelAdams 6y ago"domain logic" might be a better euphemism. Consider a library that encrypts text with AES-256. You might want unit tests that verify the IV, cypher block, plaintext and encrytped text (result) of that function. The method, "encrypt" might be your "business logic" that ought to be unit tested.
- gnulinux 6y ago"Business logic" is just another name of "logic". E.g. something like "if X is even, then print 'fizz' otherwise print 'fuzz'" is considered business logic.
- heyoo 6y agoSame point being made in the blog post. I do recommend others to read the post though - good stuff.
- jugg1es 6y agoWish I could upvote this more than once.
- thrownaway954 6y agoTHIS.... i unit test business logic since that is the core of the application and MUST work as expected. i'm not going to unit test a link that someone clicks on goes to the page they expect.
- hahamrfunnyguy 6y agoMy thoughts exactly. Unit tests are a huge help in computational-heavy portions of a project and are easy to write. The other areas of a project don't benefit as much and the tests are harder to write and keep maintained.
- Jestar342 6y agoThis kind of categorising I've always found to be orthoganal to what should really be the measure of "does it deserve a [unit] test?" . I believe the correct way to assess how much (if any) automated testing at whatever level is decided only by how valuable that thing, and the inverse of the impact of that thing going wrong, is. If you are writing on one shot script to transmute data from one format to another for say an upgrade, I don't care if you have unit tests if I am confident it has been manually tested to satisfaction. No repeatability, no regression requirement. There could and likely is value in TDD so tests might still be a thing if that is how you work. No objection there. If you are developing the plumbing code that will ensure my system adheres to financial regulations and, if it were to break, land me in jail for negligence, you can be damn sure I'm demanding a test that will be run everytime that system is built/deployed. I wrote unit tests >10 years ago for formatting a string for postal codes that I know are still run to this day on every commit because if they get it wrong there is legal recourse for the company that owns that system. It's also super quick to fix and failing at build is quicker and cheaper than failing in prod, even without the recourse. That test took me all of 1 minute to write. Bargain.
- marcosdumay 6y ago> I believe the correct way to assess how much (if any) automated testing at whatever level is decided only by how valuable that thing, and the inverse of the impact of that thing going wrong, is. Unit tests and automated tests are two completely different concept.
- Jestar342 6y agoVehemently disagree. Unit tests are subset of automated tests.
- deleted 6y ago[deleted]
- zerd 6y ago> I wrote unit tests >10 years ago for formatting a string for postal codes that I know are still run to this day on every commit because if they get it wrong there is legal recourse for the company that owns that system. If it's critical for your business I'd categorize that as business logic, not plumbing code, well deserving of unit test coverage.
- reallydontask 6y agoI really wish I had come up with this, it really neatly captures my experiences and how sometimes unit tests were really useful (Developing a (Benefit) Claims Engine which essentially did a bunch of complex calculations and then spit data out) whereas other times, unit tests just feel like a massive chore with mocks and similar stuff that add little to no value and certainly should've been at a higher level (integration or system tests) but the powers that be wanted coverage.
- nateroling 6y agoAbsolutely. One other takeaway is "write less plumbing code". Write library code that simplifies your plumbing, and unit test that.
- cylon13 6y agoThis is sort of what I've done with some success in developing games. Games in general are grossly under-tested, but there are a few good reason for that. Lots of systems can be effectively tested by just playing, and often it's tough to tease out as small of units for useful isolated testing as you would in other types of programs. What I've been doing is writing as many parts of the game as libraries as is possible, and then implementing the minimal possible usage of that library as a semi-automated test. For instance, our collision system is implemented as a library, and you can load up a "game" that has the simplest possible renderer, no sound, basic inputs, etc. and has a small world you can run around in that's filled with edge cases. This was vastly easier than trying to write automated tests for 3d collision code, and you get the benefit of testing the system in isolation, if not automatically. For other libraries like networking, the tests are much more automated, but they poke the library as a unit, rather than testing all the little bits and pieces individually.
- dmos62 6y agoComputation can be seen as plumbing. I think what you mean by "computational" is complicated plumbing.
- abernard1 6y ago> I tend to mentally divide code into roughly two types: "computational" and "plumbing". I agree with this and would go even further. Divide your code into "stateless" functional code and "stateful" objects code. Original OO was encapsulating things like device drivers that did I/O--it didn't represent data. If you don't interleave your stateless business logic with your stateful persistence, it's easy to mock "objects" that do the plumbing, and all the meat of the program is unit tests. Fwiw, the DI model (Guice, Spring, etc.) in modern Java/Scala shops closely hews to this, even if people don't mentally categorize it as such.
- danans 6y ago> Divide your code into "stateless" functional code and "stateful" objects code. IINM you are basically referring to the difference between static and instance methods in languages like C++ and Java. Putting code that neither reads nor writes the object state and instance methods is a common mistake made in both those languages. That said, both stateful and stateless code are good candidates for unit testing, especially when the code under test is a state machine, rather then just a data encapsulation mechanism.
- abernard1 6y agoNah brah. (I'm gonna say "brah" because I'm feeling especially salty) Your _program_ should have the flow of a function. At the architectural level, who-the-ef cares about about static vs instance methods in Java (I say as a person with 23 years of Java experience.) It has nothing to do with languages. You can do this in any language you want. You want to have your inputs go through a process where you have (1) INPUT state transfer, (2) some computation F(INPUT), (3) some output and state transfer, or RESULT = F(INPUT). If you do not have (1) or (3)--I hate to break it to you--but all your program does is burn CPU. If you don't have (2), your program does nothing at all. The key thing with scalable systems is they manage complexity well. If you're at the level where you're worried about "static or instance methods", you're not dealing with how data changes in large systems at all. Those words are at the level of state within a language. You need to optimize at the global systems level.
- mercer 6y agoI'd add that perhaps for this 'plumbing code', the way you describe it, gradual/static typing is a great solution.
- danans 6y ago> I tend to mentally divide code into roughly two types: "computational" and "plumbing". I think of the "computational" type more as a "deterministic data transformation" type. That applies to transformations of any data whether text, images, or the state of a machine. I think of plumbing as the movement of data without any transformation, or if a transformation occurs, it occurs at and abstracted layer that must be unit tested itself independently.
- HumblyTossed 6y agoI completely agree, but it doesn't help me hit the code coverage goal foisted upon me.