27

"I'm almost done, I'll just need to add tests!"

Booom! You did it, that was a nuke going off in my head.

No, you shouldn't just need to add tests. The tests should have been written from the get go! You most likely won't cover all the cases. You won't know if adding the tests will break your feature, as you had none, as you refactor your untested mess in order to make your code testable.

When reading your mess of a test case and the painful mocking process you went through, I silently cry out into the void: "Why oh why!? All of this suffering could have been avoided!"

Since most of the time, your mocking pain boils down to not understanding what your "unit" in your "unit test" should be.

So let it be said:

- If you want to build a parser for an XML file, then just write a function / class whose *only* purpose is: parse the XML file, return a value object. That's it. Nothing more, nothing less.
- If you want to build a parser for an XML file, it MUST NOT: download a zip, extract that zip, merge all those files to one big file, parse that big file, talk to some other random APIs as a side-effect, and then return a value object.

Because then you suddenly have to mock away a http service and deal with zip files in your test cases.

The http util of your programming language will most likely work. Your unzip library will most likely work. So just assume it working. There are valid use cases where you want to make sure you acutally send a request and get a response, yet I am talking unit test here only.

In the scope of a class, keep the public methods to a reasonable minimum. As for each public method you shall at least create one test case. If you ever have the feeling "I want to test that private method" replace that statement in your head with: "I should extract that functionality to a new class where that method public. I then can create a unit test case a for that." That new service then becomes a dependency in your current service. Problem solved.

Also, mocking away dependencies should a simple process. If your mocking process fills half the screen, your test setup is overly complicated and your class is doing too much.

That's why I currently dig functional programming so much. When you build pure functions without side effects, unit tests are easy to write. Yet you can apply pure functions to OOP as well (to a degree). Embrace immutability.

Sidenote:

It's really not helpful that a lot of developers don't understand the difference between unit, functional acceptance, integration testing. Then they wonder why they can't test something easily, write overly complex test cases, until someone points out to them: No, in the scope of unit tests, we don't need to test our persistance layer. We just assume that it works. We should only test our businsess logic. You know: "Assuming that I get that response from the database, I expect that to happen." You don't need a test db, make a real query against that, in order to test that. (That still is a valid thing to do. Yet not in the scope of unit tests.)

Comments
  • 4
    This sounds like an explanation that most beginners of unit tests would love to hear
  • 3
    Addendum: Adding tests later is only faster if you add none at all
  • 2
    Preach!
    TDD is the way to go. I think even if you write tests after you could easily get into a trap of validating your assumptions about the code you just wrote instead of making sure your code passes all of the tests you wrote prior to coding.

    Also, the number of times I have had to explain how to know what your 'unit' is is ridiculous.
  • 0
    Doing something new in a language you don't fully understand yet is not something to be done witb TDD.

    You can tell me whatever you want, but in that case: screw you!
  • 0
    @wildebeest of course, you spike til you feel comfortable, then either comment out or delete everything you just wrote (I always delete) and write tests. Still TDD'ing!
  • 0
    @unmarked oops, I was a little drunk last night...

    TDD is great for solving bugs (unit tests). But how much acceptance tests do you write?
  • 0
    @wildebeest TDD is not for solving bugs. Bugs still happen as developers forget about egde cases (I still do).

    Yet a lot of bugs also are unclear requirements that yet have to be made explicit. (The classic "that's not a bug report but a feature request".)

    Think of test-code as a living and evolving documentation of your business code giving you almost instant feedback if something breaks.

    That's why if you detect a bug (or business requirement changes), you add a test case for it first in order to fail the test. Then you make it pass again.

    TDD is about preventing regressions first due to refactoring and knowing you have a working state.
  • 2
    @wildebeest When learning a new language, I always learn the test framework first ¯\_(ツ)_/¯

    And in regards to acceptance tests, I have implemented a super booking API that abstracts away a lot of third party API. For each third party API and each of our endpoint we have at least one acceptance test, yet often more as there are loads of edge cases.

    Acceptance tests are good at telling you that something broke, but really really bad in telling what exactly went wrong and why.

    On the other hand acceptance test can really save you on code bases without unit tests.

    Once, I inherited a project without any tests. My first action was creating an acceptance test for it. Then at least I knew I broke stuff. Yet I also slowly and over time added unit test cases for the project, as I rather deal with a breaking unit than acceptance test.

    For critical parts of your application acceptance test make sense, yet unit tests should be your first line of defense. (Look up "test pyramid")
Add Comment