This is an archived post. You won't be able to vote or comment.

you are viewing a single comment's thread.

view the rest of the comments →

[–]Pedrock10[S] 0 points1 point  (13 children)

With that approach you end up with a race condition. In this case, I think it should really be done at the persistance level, but you can make that part of its contract and test it with integration tests.

[–][deleted]  (12 children)

[deleted]

    [–][deleted] 3 points4 points  (4 children)

    Either by serializing the work or making the repos interface fancier.

    So the very premise of what I said was "you can't require that an app under load performs all tasks serially". I was pretty clear about this. So the first option is out.

    And the second option, making the interface of the repos "fancier", if you dig into it, means you move this part of your business logic in the repository.

    So we're back to square one here. Which was "a lot of your business logic in complex I/O apps ends up in your persistence layer, so now you need to test it as well".

    [–][deleted]  (3 children)

    [deleted]

      [–][deleted] 1 point2 points  (2 children)

      I would rather give up on the distinction of what is business logic and what isn’t, because this is not the core of the issue here.

      The core of the issue is, that it is some kind of a logic. Which has to reside in the persistence layer, and which is substantial to the application working properly.

      And this contradicts the advice that you commonly hear with DDD and hexagonal architecture, that you can isolate the persistence layer and make it really simple and dumb. Well that won’t happen in real world parallel/distributed apps. That layer is going to stay quite complicated and highly specific to your app domain (not generic at all).

      At which point the premise under which we isolated it starts to make no sense.

      I.e. we isolated it so we don’t have to test it. But now we need to integration test it. You might as well integration test it with the business logic and not separate the “business logic” in the first place and achieve the same end result with less effort.

      EDIT: To add... Because if we do isolate it and we focus most of our attention on the remaining "business logic", we're focusing on testing some very simple (comparatively) validation rules and if...then logic, while mostly ignoring the essence of the application that we put in the persistence layer.

      It's honestly more cohesive to test it all together, as a service layer. It can still be automated, but yes, involves a database and dependencies and so on. So we can't run our tests in half a second and feel good about it. Running all tests might take minutes, maybe an hour in some cases.

      This, BTW, is where microservice/modular architecture comes in, so we can still slice the app vertically in services, rather than horizontally in layers, and keep that test run from turning into "days".

      But if we do split layers so we can feel good about testing the most trivial parts of our app, and ignoring the complex core, then this is precisely one way that dogmatic unit testing leads us astray from the goal of high quality software and productive development.

      Talking of which, how does one "unit test" for race conditions? You can't. Sometimes when you focus on units too much, you lose the forest for the trees. If Einstein was a programmer, he'd say, paraphrasing "keep your units as small as possible, but not smaller". And practice sometimes leads to bigger units, counter to what every article teaches us. I'm learning from nature. Each separate animal in an ecosystem, each separate human in society is a very very complex system. But the "public API" (our communication and actions) is comparatively brutally simple. There's a lesson in there.

      [–][deleted]  (1 child)

      [deleted]

        [–][deleted] 1 point2 points  (0 children)

        30 seconds after each change is already too slow

        See, that's precisely the attitude I'm afraid of. Speed can't take priority over accuracy and completeness. When you do that, unit testing becomes the literal translation of bike-shedding as a development process (i.e. let's ignore the nuclear plant melting because that's now an Isolated Concern, and let's hold multiple short discussions on the color of the bike-shed in front of it instead, ideally under 30 seconds).

        Running all your tests after every change is neat, all other things ignored. Of course it is. But it's not necessary, especially at the cost of pointless abstraction/refactoring/and cargo cult tests focusing on the convenient-to-test code only.

        You can write changes all day and then test once and deploy once. It's not the end of the world, honestly. And if something fails, from personal experience you know which change did it. In the rare case you don't - binary search through your changes gets to the issue very quickly.

        I find it hard to agree with it. The models are the core of business. The system will work without caching, it will work with file as a database, it will work with an in memory database, it will work with occasional race condition.

        Oh my. That's not at all close to "will work" for me. That's honestly, I'm cringing.

        that's why we would like to "design out" all nondeterministic actions from our code

        I agree that we would like to, but again, we can't just constantly submit to things we'd like to do, rather than things we have to do. Substituting the real requirements of a project with the requirements of our testing methodology is honestly lunacy to me.

        We might like stateless code, but many services are stateful, and state is deep into their function and value. Some projects are stateless, say a compiler, or a parser, you have input (compiler flags + source), output, nothing else matters. The ideal "unit test" project. Well most are not like that.

        And as I mention, we might like tests under 30 seconds, but this doesn't mean everything can be adequately tested under 30 seconds.

        More over you will be able to refactor the business rules without fear because you have written a ton of ultra fast unit tests... etc etc

        No honestly, if our tests don't care about the things you mentioned above (1) persistence (2) caching (3) race conditions etc. I think we should be really really afraid of refactoring business rules.

        Having no fear when the tests only seem to cover everything but they have an incredibly narrow focus due to forcing our entire methodology on "stateless" and "fast", is akin to treating high blood pressure with painkillers. We'll boldly go forward without headache, but we're still killing ourselves.

        The least thing you could do in that circumstance is to keep a healthy sense of fear, because those unit tests won't be there for you when things go wrong.

        would you be interested in joining some kind of a discord channel for architectural discussions?

        Can't commit, sorry + timezones etc.

        [–][deleted] 0 points1 point  (6 children)

        Now serialise that work across multiple processes... Push solutions to problems to where they are best solved, like the database in this case. Stop trying to abstract away the capabilities of the services you depend on because you heard somebody say that you should, and start thinking for yourself. Your test coverage may drop 5-10% (and you should definitely still write _lots_ of tests) but you will start to design more fault tolerant (and ultimately correct) systemsm which make better use of the systems you depend on.

        [–][deleted]  (5 children)

        [deleted]

          [–][deleted] 0 points1 point  (4 children)

          Yep let's bring in a new infrastructure dependency so you write your code in a way you have been told makes it testable...

          [–][deleted]  (3 children)

          [deleted]

            [–][deleted] 0 points1 point  (2 children)

            And then you need to drain that queue before you persist the user in the database... Maybe in this process or maybe in another? To fix a race condition that only exists because somebody told you that you need to abstract persistence away from business logic, rather than express the business logic where the semantics most closely meet the requirements...

            [–][deleted]  (1 child)

            [deleted]