• dan@upvote.au
      link
      fedilink
      arrow-up
      12
      ·
      11 months ago

      I’ve written some tests that got complex enough that I also wrote tests for the logic within the tests.

      • AAA@feddit.de
        link
        fedilink
        arrow-up
        7
        ·
        11 months ago

        We do that for some of the more complex business logic. We wrote libraries, which are used by our tests, and we wrote tests which test the library functions to ensure they provide correct results.

        What always worries me is that WE came up with that. It wasn’t some higher up, or business unit, or anything. Only because we cared to do our job correctly. If we didn’t - nobody would. Nobody is watching the testers (in my experience).

    • kevincox@lemmy.ml
      link
      fedilink
      arrow-up
      6
      ·
      11 months ago

      Mutation testing is quite cool. Basically it analyzes you code and makes changes that should break something. For example if you have if (foo) { ... } it will remove the branch or make the branch run every time. It then runs your tests and sees if anything fails. If the tests don’t fail then either you should add another test, or that code was truly dead and should be removed.

      Of course this has lots of “false positives”. For example you may be checking if an allocation succeeded and don’t need to test if every possible allocation in your code fails, you trust that you can write if (!mem) abort() correctly.

      • Lifter@discuss.tchncs.de
        link
        fedilink
        arrow-up
        1
        ·
        11 months ago

        Right,too much coverage is also a bad thing. It leads to having to work on the silly tests every time you change som implementation detail.

        Good tests let the insides of the unit change without breaking, as long as the behave the same to the outside world.

  • Alexc@lemmings.world
    link
    fedilink
    arrow-up
    62
    arrow-down
    8
    ·
    11 months ago

    This is why you write the test before the code. You write the test to make sure something fails, then you write the code to make it pass. Then you repeat this until all your behaviors are captured in code. It’s called TDD

    But, full marks for writing tests in the first place

  • vsh@lemm.ee
    link
    fedilink
    English
    arrow-up
    30
    arrow-down
    1
    ·
    11 months ago

    I don’t need tests when I know the output 😎

  • jbrains@sh.itjust.works
    link
    fedilink
    arrow-up
    22
    ·
    edit-2
    11 months ago

    This seems to happen quite often when programmers try to save time when writing tests, instead of writing very simple tests and allowing the duplication to accumulate before removing it. I understand how they feel: they see the pattern and want to skip the boring parts.

    No worries. If you skip the boring parts, then much of the time you’ll be less bored, but sometimes this will happen. If you want to avoid this, then you’ll have to accept some boredom then refactor the tests later. Maybe never, if your pattern ends up with only two or three instances. If you want to know which path is shorter before you start, then so would I. I can sometimes guess correctly. I mostly never know, because I pick one path and stick with it, so I can never compare.

    This also tends to happen when the code they’re testing has painful hardwired dependencies on expensive external resources. The “bug” in the test is a symptom of the design of the production code. Yay! You learned something! Time to roll up your sleeves and start breaking things apart… assuming that you need to change it at all. Worst case, leave a warning for the next person.

    If you’d like a simple rule to follow, here’s one: no branching in your tests. If you think you want a branch, then split the tests into two or more tests, then write them individually, then maybe refactor to remove the duplication. It’s not a perfect rule, but it’ll take you far…

    • ChickenLadyLovesLife@lemmy.world
      link
      fedilink
      English
      arrow-up
      27
      ·
      11 months ago

      the code they’re testing has painful hardwired dependencies on expensive external resources

      I’ve told this story elsewhere, but I had a coworker who wrote an app to remote-control a baseball-throwing machine from a PDA (running WinCE). These machines cost upwards of $50K so he only very rarely had physical access to one. He loved to write tests, which did him no good when his code fired a 125 mph knuckleball a foot over a 10-year-old kid’s head. This resulted in the only occasion in my career when I had to physically restrain a client from punching a colleague.

      • jbrains@sh.itjust.works
        link
        fedilink
        arrow-up
        6
        ·
        11 months ago

        Wow. I love that story and I’m glad nobody was hurt.

        I wonder whether that happened as a result of unexpected behavior by the pitching machine or an incorrect assumption about the pitching machine in that coworker’s tests.

        I find this story compelling because it illustrates the points about managing risk and the limits of testing, but it doesn’t sound like the typical story that’s obviously hyperbole and could never happen to me.

        Thank you for sharing it.

        • ChickenLadyLovesLife@lemmy.world
          link
          fedilink
          English
          arrow-up
          15
          ·
          11 months ago

          It happened because the programmer changed the API from a call that accepted integer values between 0 and 32767 (minimum and maximum wheel speeds) to one that accepted float values between 0.0 and 1.0. A very reasonable change to make, but he quick-fixed all the compiler errors that this produced by casting the passed integer parameters all through his code to float and then clamping the values between 0.0 and 1.0. The result was that formerly low-speed parameters (like 5000 and 6000, for example, which should have produced something like a 20 mph ball with topspin) were instead cast and clamped to 1.0 - maximum speed on both throwing wheels and the aforesaid 125 mph knuckleball. He rewrote his tests to check that passed params were indeed between 0.0 and 1.0, which was pointless since all input was clamped to that range anyway. And there was no way to really test for a “dangerous” throw anyway since the machine was required to be capable of this sort of thing if that’s what the coach using it wanted.

          • Duralf@lemmy.world
            link
            fedilink
            arrow-up
            5
            ·
            11 months ago

            API from a call that accepted integer values between 0 and 32767 (minimum and maximum wheel speeds) to one that accepted float values between 0.0 and 1.0.

            This would cause alarm bells to ring in my head for sure. If I did something like that I would make a new type that was definitely not implicitly castable to or from the old type. Definitely not a raw integer or float type.

            • marcos@lemmy.world
              link
              fedilink
              arrow-up
              4
              ·
              11 months ago

              That kind of code usually is written on a restricted dialect of C.

              C is not a language that allows for that kind of safety practice even on the fully-featured version.

              • Duralf@lemmy.world
                link
                fedilink
                arrow-up
                3
                ·
                11 months ago

                Even in C this is possible. Just wrap the float or whatever in a struct and all implicit conversions will be gone.

              • jbrains@sh.itjust.works
                link
                fedilink
                arrow-up
                1
                ·
                11 months ago

                Indeed, this is a time for naming conventions that communicate the details that the type system can’t clarify. This leads to the long names that senior programmers make fun of. Don’t listen to them; let them laugh then make this kind of mistake.

                • marcos@lemmy.world
                  link
                  fedilink
                  arrow-up
                  1
                  ·
                  11 months ago

                  This leads to the long names that senior programmers make fun of.

                  Hum… The notation that I’ve seen people making fun of is one where the long names encode the exact same information that C types can handle for you and nothing else. But YMMV.

                  Anyway, I don’t think any naming convention can save you after somebody goes over your entire codebase converting things without care for the semantics. If you are lucky, it’s one of the lazy people that do that, and you will “only” have to revise tens of thousands of lines to fix it. If you are unlucky, the same person will helpfully adjust the names for you too.

          • jbrains@sh.itjust.works
            link
            fedilink
            arrow-up
            3
            ·
            11 months ago

            Yikes! That’s also a great cautionary tale for Primitive Obsession/Whole Value as well as a bunch of other design principles.

            I’m thinking about how I’d have done that refactoring and now I wish I had the code base to try it on. It sounds like it would make a really good real-life exercise in a workshop. “Remember folks, you have to get this right. There’s not really a way to check this with the real hardware, and if you get it wrong, someone’s going to get hurt.”

            Thanks again.

      • Victoria@lemmy.blahaj.zone
        link
        fedilink
        arrow-up
        16
        ·
        11 months ago

        fun situations can arise when you write , instead of ; For those not in the know, in c++ the comma operator evaluates the left expression, discards the value, then evaluates the right expression and returns the value. if you now have a a situation like this

        int i = 0,
        printf("some message");
        

        i has a completely different value, since it actually uses the return value of printf instead

  • Rin@lemm.ee
    link
    fedilink
    arrow-up
    11
    ·
    11 months ago

    now you have a group of very specific tests for later debugging

  • I Cast Fist@programming.dev
    link
    fedilink
    English
    arrow-up
    13
    arrow-down
    3
    ·
    11 months ago

    I remember being asked to make unit tests. I wasn’t the programmer and for the better part of a week, they didn’t even let me look at the code. Yeah, I can make some great unit tests that’ll never fail without access to the stuff I’m supposed to test. /s

    • loutr@sh.itjust.works
      link
      fedilink
      arrow-up
      11
      ·
      11 months ago

      I guess it would make sense if you’re testing a public API? To make sure the documentation is sufficient and accurate.

      • Natanael@slrpnk.net
        link
        fedilink
        arrow-up
        9
        ·
        11 months ago

        Yeah blackbox testing is a whole thing and it’s common when you need something to follow a spec and be compatible

      • folkrav@lemmy.ca
        link
        fedilink
        arrow-up
        2
        ·
        11 months ago

        He specifically said “unit tests” though, which aren’t black box tests by definition

  • fiveoar@lemmy.dbzer0.com
    link
    fedilink
    English
    arrow-up
    11
    arrow-down
    3
    ·
    11 months ago

    Contrats, you have discovered why in TDD you write the test, watch the test fail, then make the test pass, then refactor. AKA: Red, Green, Refactor

      • fiveoar@lemmy.dbzer0.com
        link
        fedilink
        arrow-up
        1
        ·
        edit-2
        11 months ago

        Sure, so say so you have a requirement to add two numbers

        You write a test that has two two inputs and the output Run the test and watch it fail, check that the output is what you expect so you know the test is working

        At this point you have no implementation and you use this opportunity to confirm that the test will work, by checking it is failing how you expect. If you are pairing sometime I teach that you should call out what you expect, kinda like in American pool. Sometimes the test passed in this case, this is your opportunity to break the test and confirm it will fail (though this is often a sign you did too much work previously, and might need to check if you really are making the smallest possible change)

        Do the minimum to correct the problem described by the failing test (you can follow the transformation priority premises here if you are familiar with it)

        At this point you have only implemented the simplest possible code, this makes it really easy to spot if there is a problem with it because of some flaw in the test, and you have confirmed the it matches your test

        What’s more you can confirm all, and only behaviours described in the test are implemented

        Look at the code and decide if you can simplify it, do any refactoring

        Got to clean the kitchen because if we don’t clean the kitchen we will have to clean the garage and we don’t want that because it’s a bigger job.

        repeat

        Why this works is that the code is developed in a TDD style forces you to move in smaller steps meaning bugs are shallower when they do occur. You aren’t dealing with a 20 complex lines, you are dealing with a return const, or selection, or etc. The scope for the test being wrong is reduced and the amount of implementation is reduced, generally the tests end up more concise and smaller too and the interfaces are user friendly too because you didn’t think how do I calculate this, you thought what would be a nice way to call this.

        What’s more it encourages an example driven approach that leads to developers thinking about the most sensible input data over and over again, and what that should output reducing the chance any one wrongly implemented test wouldn’t be picked up by other examples.

        TL;DR, the driven word is the key, a test that is illogical will never drive you to the working code

  • SrTobi@feddit.de
    link
    fedilink
    arrow-up
    4
    ·
    11 months ago

    And then in the end we realize the most important thing was the tests we wrote along the way.

  • Alph4d0g@discuss.tchncs.de
    link
    fedilink
    arrow-up
    1
    ·
    11 months ago

    I’ve seen some interesting thoughts on TDD with fail, pass, refactor assumptions. I’m curious if anyone here is writing functional code in order to then make a failing functional test pass i.e. BDD / ATDD. This follows similar logic without the refactor assumption. I’ve seen strong opinions on every side as far as this is concerned. On a team with Dev and QA competencies, I’ve heard a number of devs glad to get QA out of the bottleneck and put their knowledge to better use.

    • organicmolecules@lemmy.ml
      link
      fedilink
      arrow-up
      5
      ·
      11 months ago

      Depends. If I’m working in an existing system and I know what the shape of the thing I’m writing is, then I might write the test first and tdd it out as that process is usually a bit faster for me.

      If I’m developing a new feature I’d probably spike out a solution and write an acceptance test to match it, then if I’m feeling pedantic I might throw away the spike code and tdd it back up from scratch but I haven’t done that in a while now.

      This all depends on the language and the abstraction layer I’m at.