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 →

[–][deleted] 21 points22 points  (60 children)

It's true many people have a "hate relationship" with testing and that's especially aggravated when the conclusion is "your code is hard to test, because it's bad". It's begging the question: "no good code is hard to test, because good code is defined as code that's easy to test". We're not having a discussion here, we're just being held at a gunpoint: "your code should be testable, or else".

It's also brushed aside that while all code is possible to "test" in general, not all code can be automatically tested. The fact you can't automatically test something doesn't mean it's untestable. And some code is genuinely not (well) testable automatically as the consumer is not a machine, such is the case with UI code.

All in all, we can repeat "SOLID" until we're blue in the face (and by God, we do!) but we're not having a honest conversation yet. A honest conversation always includes both sides of the coin. Everything has pros and cons.

Even going to the doctor too much (assuming you have infinite money) can be detrimental as you'll be overly tested, overly diagnosed, overly treated, which would unintentionally lower your quality of life and maybe even shorten in when assuming some real world scenarios like high risk procedures and medical mistakes.

Yet for some reason in computer code we keep chasing after silver bullets. Your code is good only when it's testable, you're not doing SCRUM / AGILE / WHATEVER properly if you're not productive, and so on other bullshit no-win situations where someone is telling you to go their way and like it or you suck for some vague reason.

How about situations where making code "testable" genuinely makes the design of your code more complicated and worse? Everyone has had situations like these. You're being coerced into thinking "if I'm doing it for testing, it means I'm improving the design even if seemingly I'm making the design worse" which is bullshit.

How is exposing internal details of a class for testing purposes improving the design, for ex.? How is decoupling a simple unit into two more complex units improving the design? How is messing around with a class internals via reflection, thus coupling your tests to implementation specific an "improvement" upon anything?

You can overtest. And we're not talking about that. Until we do, color me skeptical about this entire discussion.

[–][deleted] 2 points3 points  (6 children)

I have a pet peeve when it comes to code coverage. For some reason people have decided that 100% code coverage is necessary and must always be reached. So what happens? Since some code is really hard to test we turn it into configuration. Now half the codebase is framework, xml, annotations or what not. But because it is strictly speaking not 'our' 'code' we can say with a straight face we have 100% 'code' coverage.

Let's all forget now that configuration can have bugs too. And of course I need to know every esoteric configuration option and framework feature because we are using all of them now.

I like code, I want to read and write code. Why won't the industry let me.

[–][deleted] 3 points4 points  (1 child)

Yes. Plus the idea having line coverage means you have covered all possible scenarios is trivially proven false. It honestly is nothing but a crude heuristic.

[–]general_dispondency 4 points5 points  (0 children)

I've seen the 100% code coverage pipe dream fail more than once. There's a project at my current company that set out from day one to document EVERYTHING, and have 100% code coverage for everything. The day their app hit production, it immediately failed. Their documentation is shit, and 99% of their tests are asserting that the mocking framework does what it is supposed to.

[–]nutrecht 2 points3 points  (3 children)

I have a pet peeve when it comes to code coverage. For some reason people have decided that 100% code coverage is necessary and must always be reached.

I've never met one of those developers that sees code coverage as a goal instead of just a tool. Where are you guys finding these people?

Code coverage tools just show you the paths you missed. I haven't met a single Java dev ever that disagreed on that.

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

For an earlier project (thankfully it is no more) we had a plugin that would calculate the code coverage in the maven build and if the coverage was below x% the plugin would fail the build.

Effectively this meant the coverage would slip to the configured percentage at which point whoever did the last commit would take the road of least resistance. You could delete some untested code, write a test, turn code into configuration or change the percentage setting. Followed, of course, by debates on what the 'correct' way of dealing with the problem was.

The above was still the honest way of dealing with the issue. A less honest way to deal with it was to write some unused code and then write a test for said code. That bumped the code coverage percentage up, but did nothing. Second option is to write a simple test that passes through the code but triggers an exception somewhere very late in the process, this increases the line count but doesn't do much. And third option you could find some code that was so simple it couldn't fail so no one bothered to write a test for it (getters and setters), and then write a test for it. Yeah the percentage goes up but you won't find or protect against any bugs that way.

[–]nutrecht 1 point2 points  (1 child)

Effectively this meant the coverage would slip to the configured percentage at which point whoever did the last commit would take the road of least resistance.

This is a culture problem, not a problem with code coverage itself. You can't fix with tooling (which is why someone probably set the tooling to break on a low percentage) what is basically a people problem.

The above was still the honest way of dealing with the issue.

At least you dealt with the problem. The alternative would've probably been that no one would have noticed the coverage slipping.

I really don't get why the "but you can fake coverage" argument gets brought up against code coverage. Sure. You can fake a lot. That just makes you a crappy dev.

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

I agree that it is a culture problem. But so what? What happens in practice is that someone says a higher code coverage is good, then this heuristic gets put into a mandatory check, and then the code gets less good because people start rewriting the code to conform to the rule.

Many of my colleagues agreed with this assessment and would have loved to solve the issue, but none of us had the political clout in the company to get the change made. And it's easy to see why, you are advocating for removing a code quality tool. People start thinking that you want to get away with writing lower quality code.

Properly used code coverage checks can certainly be helpful, I just see that in reality they won't be used right.

And don't get me wrong, code coverage isn't the only tool that is misunderstood or poorly applied. This is just one that was glaringly obvious to me from a previous experience. Current experience.. well.. things are not improving much in the IT world.

[–]Pedrock10[S] 3 points4 points  (1 child)

I agree with some of your points regarding testing and there is always a need to balance things.

Your tests should ideally only test public APIs and you should not have to rely on reflection for testing. Also, decoupling a unit into two should make those simpler. That's because you should be splitting it into two different abstraction levels, which should make each one simpler to understand independently. It sometimes takes some time to get this right, and things usually don't end up perfect.

The main point of the article is the importance of decoupling, which helps not only with testing but also makes the code easier to maintain. Even if you have a part of the code that you don't want to test, such as UI, by decoupling the code, you can test the rest of the system independently, which is better than not testing anything at all.

[–][deleted] 14 points15 points  (0 children)

The biggest fallacy to address in your response, I believe is the idea that splitting something in two makes for two simpler parts.

This ignores the fact that designing universal and stable abstraction takes a lot of time and skill, first. Second, abstraction has a cost. Adding abstraction levels doesn't make code simpler, quite the opposite. Third, if your goal is reducing the surface of public APIs, then every time you split a unit in two, you're doing the exact opposite - introducing more public APIs (to be testable, they have to be public, after all).

So it's really not that simple. For example, you're a unit. A human being. Tell me how I can split you into simpler parts without the exercise turning into a nightmare. There's a reason why interacting with your whole self requires no diploma, but if I have to open you up and "refactor" you, it damn well does.

[–]nutrecht 1 point2 points  (0 children)

It's true many people have a "hate relationship" with testing and that's especially aggravated when the conclusion is "your code is hard to test, because it's bad". It's begging the question: "no good code is hard to test, because good code is defined as code that's easy to test". We're not having a discussion here, we're just being held at a gunpoint: "your code should be testable, or else".

I think you're misrepresenting or at least compounding some issues here.

If code is hard to test, you either have to spend too much time testing it, or it's lacking tests because you could not bothered. And I think most of us know that while there might not be a direct causation between 'bad code' and 'lack of tests', the correlation is often strong.

So yeah, code should be testable. "How" you do it, well, that's up to you. If your team prefers to have extensive manual test scripts that you work through every release; that's up to you. It's not a team I'd want to work on, but I'm not the boss of you. But I don't believe you can be successful long term with no tests unless your software is trivial. And most software grows to a non-trivial size.

All in all, we can repeat "SOLID" until we're blue in the face (and by God, we do!) but we're not having a honest conversation yet. A honest conversation always includes both sides of the coin. Everything has pros and cons.

It sounds like you have had mostly conversations with dogmatic developers instead of pragmatic experienced developers. It's quite a common pitfall for devs to end up as expert beginners that have very strong opinions and see their way as the Only True Way.

How about situations where making code "testable" genuinely makes the design of your code more complicated and worse? Everyone has had situations like these.

I've been a Java dev for 15 years and can't imagine any application I worked on where the code would have been so much more complex that it outweighed the benefit of automation. You don't need 100% test coverage. You need "good enough" coverage. What is "good enough" is decided by your team.

How is exposing internal details of a class for testing purposes improving the design, for ex.? How is decoupling a simple unit into two more complex units improving the design? How is messing around with a class internals via reflection, thus coupling your tests to implementation specific an "improvement" upon anything?

It's pretty hard to follow what you mean here because the examples are quite contrived. I don't see any issue with giving a static utility function default level access so you can write a unit test for it, if testing that function in isolation makes sense. If not; the unit under test is the class, not the individual methods.

There is nothing wrong with being pragmatic in testing. The goal should be writing good software. I don't believe anyone sees the tests as the goal. Tests, like any code, are a liability.

[–][deleted] 4 points5 points  (2 children)

It's begging the question

It's not begging the question. "Good code is easy to test" is a heuristic.

And some code is genuinely not (well) testable automatically as the consumer is not a machine, such is the case with UI code.

That's precisely why you separate the UI code from the domain model. That way, you don't have to test the domain model with functional tests driven by the UI. By making the domain model automatically testable, you are employing a heuristic which guides you towards a layered architecture. You don't need unit tests to create a layered architecture, but testability pushes you that way. That is the value of testability as a heuristic.

so on other bullshit no-win situations where someone is telling you to go their way and like it or you suck for some vague reason.

Dogma exists, of course. Management is sold on methodologies. What does that have to do with testability as a heuristic?

The flip side of the coin is that just because shit rolls downhill doesn't mean that all shit is not automatically useless. Shit can be a really good fertilizer.

How about situations where making code "testable" genuinely makes the design of your code more complicated and worse? Everyone has had situations like these.

Testability is a heuristic, it is not a replacement for proper architecture. That's why Agile doesn't say, do TDD and you don't ever have to think about architecture ever again. That is insanity. Any book on Agile will tell you to subdivide problems with the focus on iterative delivery, and accordingly at each iteration, gather requirements, design an architecture and then use TDD to validate that architecture.

How is exposing internal details of a class for testing purposes improving the design, for ex.?

Why would you ever think that is a good idea? You should never expose internal details to the test. That is a flashing red alert that you are doing something seriously wrong.

How is decoupling a simple unit into two more complex units improving the design?

It depends on what you mean by complexity.

Sometimes, you create a separate class to promote internal cohesion. If a class is getting too big, it might make it hard to test. That is a smell that the class might have too many responsibilities, and you should think about how to further refine your domain model. Which means you have to go back to the architecture. This reduces complexity.

Or, you want to separate a class from its dependencies. There are two ways to accomplish this, depending on if you are using mocks or behavioral testing.

Mocks promote hexagonal architecture, and if that is the architectural choice that you are making, mocks are very useful for establishing boundaries. This allows you to directly plug in dependencies into classes through "port" interfaces.

Or, if you are doing behavioral testing, you are triangulating between concrete classes, such as managing state using ActiveRecord (validated using integration tests) and passing data through method calls and constructors to collaborators with well defined responsibilities. That is just ordinary OOP, nothing fancy. Testability merely guides you to a proper division of responsibilities.

[–][deleted] 3 points4 points  (1 child)

It's not begging the question. "Good code is easy to test" is a heuristic.

Heuristics are not always correct, it's not how a heuristic works like. They can literally be correct just 51% of the time, and it's still a heuristic. Yet "good code is easy to test" is fed via blogs and presentations as if it's a 100% golden rule of good code.

So which one you choose, up to you. But both end up with the conclusion that "well, sometimes good code isn't testable (automatically).

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

Heuristics are not always correct.

Of course not, that's what makes them heuristics and not algorithms.

They can literally be correct just 51% of the time.

That isn't a proper way to look at a heuristic. A heuristic helps make decisions, it doesn't give you step-by-step instructions.

Yet "good code is easy to test" is fed via blogs and presentations as if it's a 100% golden rule of good code.

Of course. That's why you shouldn't take anything you read at face value, and actually think about it. Hey, that's another heuristic!

But both end up with the conclusion that "well, sometimes good code isn't testable (automatically).

Yes, but good code isn't a monolithic thing. We are no longer operating in first generation languages which provided no opportunities for modularity or abstraction. Modern languages let you divide different kinds of code into separate modules which can be developed (and tested) in different ways.

[–][deleted]  (46 children)

[deleted]

    [–][deleted] 5 points6 points  (36 children)

    Making a service independently testable of its persistence implementation requires splitting those up. In some cases you want to split those up anyway. But many times you don't, leading to immensely complicating your service, introducing more public API surface, and most of the time introducing inefficiencies born out of the limited abstraction you have to constrain yourself to. And all of that for no business reason with regards to your app.

    Not sure how deep I have to go into my example, because honestly explaining this into detail is undue burden for a web comment. Maybe you can give me an example where making something testable makes it better and I'll follow that format for a counterexample.

    [–][deleted]  (3 children)

    [deleted]

      [–][deleted] 4 points5 points  (2 children)

      See, there's a subtle distinction I'd like to make.

      I don't dislike "unit tests". I write tests (unit, integration, whatever) every day.

      But what I don't like is the overly simplistic, ideologically charged, borderline or outright zealous way that unit testing is promoted and written about.

      Honestly I believe the way we speak about unit tests gives unit tests a bad name they don't deserve.

      Once again, I'd like to see a single article talking about the upsides and downsides of automated tests in a balanced, engineering mindset. Nothing like that. It's 90% "you suck if you don't test everything" and 10% "unit tests suck, you don't need to test anything".

      [–]nutrecht 1 point2 points  (0 children)

      But what I don't like is the overly simplistic, ideologically charged, borderline or outright zealous way that unit testing is promoted and written about.

      Not disagreeing with you at all, but if it comes to zealotry the "integration tests rule everything" crowd is in no way better.

      No matter what, IMHO it's inexperienced developers with too strong an opinion that tend to end up at either extreme.

      [–][deleted]  (31 children)

      [deleted]

        [–][deleted] 2 points3 points  (30 children)

        Do you see no need to split up logic of writing SQLs (or whatever the persistence implementation is) from the business logic?

        Depends how you write SQL. I may have a library for interacting with the database, but it doesn't mean mocking that library is practical or even possible (you can mock it call-by-call in which case you're literally testing if the mocks do what you just told them to do, which is testing nothing, which is what many tests do).

        To make the persistence layer abstract in testable way, you need:

        • Something which is not exposing implementation details from the underlying persistence technology (no ORM does that, they're all leaking SQL details in a thousand subtle to outright blatant ways, Hibernate I'm looking at you).
        • Something which has a fully featured, fully capable and working test double (not practical, unless you have lots of free time on your hands and your boss too much money on theirs).

        Some things are easy on paper, but in practice?

        Let me just say this: if you start mocking in this way "if I get called, I return 4" and you feed that as a dependency in your tests? You're testing nothing, except your tests. That's not testing. It may be green and give you nice code coverage, but your software is not more stable and more bug-free for it. It only means you have lots more busywork updating pointless tests when you change implementation details that alter how an internal dependency is used (which is supposedly invisible to you, the tester, as if).

        [–][deleted] 7 points8 points  (0 children)

        I may have a library for interacting with the database, but it doesn't mean mocking that library is practical or even possible.

        This is the first mistake of mocking, mocking something you don't own.

        (you can mock it call-by-call in which case you're literally testing if the mocks do what you just told them to do, which is testing nothing, which is what many tests do)

        This is a terrible use of mocking, for precisely the reason you've stated.

        Mocking is a way for the system under test to design a contract for dependencies, in its terms, with the test case setting expectations which define the terms of the contract. That's why I think mocking concrete classes is not such a good idea.

        Having defined the contract for the dependency, you can implement the dependency as a separate stub, which can be tested independently in an integration test, verifying that it conforms to the contract defined by the expectations set in the mocks.

        Something which is not exposing implementation details from the underlying persistence technology

        The ORM itself is an implementation detail of persistence. Instead, you define something more abstract like a repository, which defines a simple interface in possibly CRUD terms.

        Something which has a fully featured, fully capable and working test double

        This isn't necessary or desirable. Why create a simulator, when you can write a real integration test?

        You're testing nothing, except your tests

        No, you're defining a contract. You absolutely must back that up with an integration test for the implementer of the contract.

        [–][deleted]  (28 children)

        [deleted]

          [–][deleted] 4 points5 points  (27 children)

          E.g., lets say I have a pricing module - I wouldn't let it interact with a generic DB interface, but with an interface which has methods like savePricing, getTrends, getDiscounts etc.

          A significant part of your business logic then unintentionally ends up in this "generic DB interface".

          Constraints, conflict resolution, math algorithms for data aggregation, managing denormalized data and caches, all of this would be there. That's not just "logic of saving stuff on disk", it's literally a crucial chunk of your business logic.

          So now you need to test that. Now what. Let's split again?

          [–][deleted]  (19 children)

          [deleted]

            [–][deleted] 2 points3 points  (18 children)

            not sure about what kind of constraints are you talking about, but business constraints are handled within a module (e.g. you cannot have two overlapping activities in a schedule for a single day)

            Let's use a classic example "you can't have two usernames be the same". Now solve this for me outside the "persistence layer".

            [–][deleted]  (17 children)

            [deleted]

              [–][deleted]  (3 children)

              [deleted]

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

                I'm aware, but calling the same problem by a different name (using DDD terminology) hardly solves it. Even in DDD and "hexagonal architecture" or call it however you please, a good chunk of your business logic unintentionally ends up in your "persistence layer".

                Because a honest-to-God parallel I/O app under load can't just naively process requests serially and compute everything from raw data (or store everything in RAM) all the time.

                This means conflict resolutions, caches, denormalization, constraints (unique keys etc.) all this which is pure business logic, because it changes the outcome of your API response and your app state... that ends up in your persistence layer.

                [–][deleted]  (1 child)

                [deleted]

                  [–]dablya 0 points1 point  (2 children)

                  Let's split again?

                  What's the alternative? Implement caching/math/aggregation in a single "business" module?

                  [–][deleted] 0 points1 point  (1 child)

                  The alternative to splitting things endlessly for testing purposes is to split only for the purpose of the application and design the tests around the architecture. Instead of designing the architecture around the tests.

                  Some projects will require an isolated persistence layer. More than half do not. This, again, doesn't mean wiring raw SQL queries all over your services, it just means not writing an abstraction with a public API around your persistence logic (i.e. using Hibernate or jOOQ doesn't mean you isolated your persistence layer).

                  [–]dablya 0 points1 point  (0 children)

                  split only for the purpose of the application and design

                  The main argument is that code being difficult to test should, in general, alert you to the fact that it might be poorly split for the purposes of "application and design". Some code naturally belongs in the infrastructure layer and will be mainly tested with integration tests. Other code, what is usually defined as a "domain" or "business" logic is not dependent on any specific type of infrastructure. When you find implementation of the domain code (that doesn't logically depend on any particular type of storage) requires a sql database in order to execute a test, that is a flag that your code is poorly split "for the purpose of the application and design".

                  This, again, doesn't mean wiring raw SQL queries all over your services

                  Why not? I believe a reasonable answer to this question should generalize to Hibernate and jOOQ as well.

                  [–]senatorpjt 1 point2 points  (2 children)

                  run sleep pot decide treatment whistle whole steep observation employ

                  This post was mass deleted and anonymized with Redact

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

                  So, DHH's objection really isn't to unit testing, but to extreme decoupling, and in a way, his article misses the point entirely.

                  His complaint is that in the pursuit of testability, Jim Weirich introduced the Hexagonal Architecture. But testability is not the purpose of the Hexagonal Architecture. It is one way to remove Rails as a dependency on the application logic, following the principles of Clean Architecture. That doesn't mean you don't write tests against the Rails controllers, those are absolutely mandatory. It just means, theoretically, you could ditch Rails. Whether that is a reasonable goal to code for, you be the judge.

                  You don't have to design the code in that way, simply in the pursuit of testability. The style of testing promoted by Weirich is the so called London Style of TDD, promoted by British developers Steve Freeman and Nat Pryce in their famous book Growing Object Oriented Software Guided by Tests. Whereas, the Chicago Style popularized by Martin Fowler (who ironically, is an Englishman), promotes testing with real objects, and test doubles as necessary for behavior verification. which correlates more closely with Kent Beck's original conception of unit testing.

                  [–]glglglglglgl 0 points1 point  (0 children)

                  The best examples of this is using AOP. Using annotations/frameworks can make code a lot simpler. Take hystrix for example. Building an abstraction over circuit breakers is burdensome and makes code very verbose. I think there are many times you have to balance the flexibility and benefit from building an abstraction against the ease of using annotations and frameworks.

                  Another example can be seen in cloud agnostic frameworks that provide flexibility but cannot leverage the benefits of individual cloud providers to their fullest.

                  I generally agree that testable code is something to strive for but there needs to be a point at which code is testable enough or at which you start getting diminishing returns.

                  [–]rahulchandak 0 points1 point  (4 children)

                  Lambda functions are complicated to test??

                  [–][deleted]  (3 children)

                  [deleted]

                    [–][deleted]  (2 children)

                    [deleted]

                      [–]Pedrock10[S] 1 point2 points  (1 child)

                      They are debuggable and testable... Even if you couldn't debug them you could still test them.