loading...

Do you even unit test, bro ?

juyn profile image Xavier Dubois 🇫🇷 ・4 min read

I don't always test my code, but when I do, I do it in production

Unit test is rocket science

A lot a coders don't write any unit tests. The main reason if that they don't understand why the should spend time on this. It's just a waste of time, right ?

I was exactly like that, then my code start crashing in production. On big and visible projects. So, I looked for a way to be more confident about my code, about my commits.

Unit test was the first answer, but it took me time before writing them correctly.

What is a good unit test ?

To write a good, rock solid and trusty unit test, you need to:

  • Understand what you are testing. That may sound stupid or obvious, but it's not. Read the method, understand what it does, how and why. How can you test it if you don't know what it should or shouldn't do ?

  • Avoid the "over mocking" syndrom. Mocking make writing test easy, but it's not a good practice. Only mock if you don't have any other choice, like for an API call. And, please, don't stub the class you are testing !

  • SEAT. Setup, Exercises, Assert, Teardown. And repeat for each tests.

  • I/O. Focus on data transformation. Defined a start state, run the test, assert on the output. What happened during the process, or how it happened is not your concern.

  • Decide a scope. Stick to it. A method can have as many tests as you want, but try to separates tests in use case scenarios.
    You want to assert that a method transform an array to json, but also throw an exception ? Alright, make it 2 independant tests !

Does code coverage really matter ?

TL;DR: No. It does not.

I've heard a LOT of people saying that you should aim for 100% code coverage. But that's a bad idea.
Don't get me wrong, the higher code coverage you have, the better. But it should not be your main concern, neither your objective.

I ask myself a few questions before writting a test for a method:

  • Does the method transform the data ?
  • Can I write this test without mocking what the method will output ?
  • Does the method returns something at all ?
  • Can I make an assertion on the result ?

If any of this questions is anwsered by a no, it is likely I'll not write any test.

It's not because you have 100% code coverage that your code is tested properly.

You shall not pass !

You shall not pass !

Code is composed by public methods, yes, but also by private and protected ones.
Should we test them ?

Of course not. But also yes !

They usually contains the business logic, so, yeah, test them. But, unlike public methods, the are not worth a specific test.
The purpose of a private method it's to be called by a public method of the same class. Let's apply the same logic inside our tests. Private and protected methods should be test through public method's tests.

Always be F.I.R.S.T

I am used to write tests following the FIRST principles, but other methods exists, like AAC for example.
I assume that, as a developper, you know how to use Google to find them if FIRST is not a good fit for you !

Timey wimey dr who

  • Fast: A test should be light fast. If not, I'll discouraged you or your team from running it. Big project may have thousands of tests and assertations.
    Setup, test and tears down should not take more than a few milliseconds

  • Independent: Tests must not depends on each other, or on the state of a previous test.
    All data needed should be created for the current test, and destroy afterward.
    If you need the same data for many tests, just create it during setup.

  • Repeatable: Tests should be repeatable over and over. Time, date or zone should not affect them.
    Yes. You can be a Time Lord and mock Time (unless it's a fix point in time, of course ;-) )
    Remember, if a test failed, the only valid reason should be that the method is not working as expected. Or that the test is wrong. But that's all folks !

  • Self-validating: No manual inspection required to check whether the test has passed or failed.

  • Thorough: Don't be lazy. Unit testing should take time to write, sometime even more than writing the method itself.

    • Test every use case scenario
    • Don't limitate yourself for simple values. Go for edge values
    • If the methods can process bulk data, test if with a large data set
    • Tests for exceptions and errors
    • Don't only test for success. You can assert a faillure, and you should !

Conclusion

Keep your tests simple, limit them to a specific scope and focus on the output of the method.

Unit test is just the first step for a complete test cycle. Take a look on the pyramid bellow.

Idea test pyramid

Discussion

pic
Editor guide
Collapse
bootcode profile image
Robin Palotai

TDD aside, the reason why we do or don't write unit tests depends on our experience.

Novice developers don't write tests since they believe their code is correct. Or they don't care if their code is correct. Or they don't know they should care. When they face enough edge cases and production bugs, they start to appreciate them.

I'll run a learning to drive analogy - novice developers are like driving students, bumping into stuff on the crash course every now and then.

Somewhat experienced developers are like drivers with a fresh licence - best practices still afresh, taking good care. Writing unit tests and all. Did you know that fresh drivers have low accident rate, until after 2 years in?

Experienced developers have confidence and don't fall into pits that often. Those 2 years have passed, and they are getting a bit reckless. Why write tests if I really trust my code? So they don't, and everything works fine for a while.

Until everything gets super-messy. Then they learn the second purpose of testing: to prevent code that was once working to be ruined.

Without testing, a future developer (maybe your future self) will inevitably change some detail that alters previous functionality. This might stay hidden for a while, only to be twice as surprising when the fireworks erupt.

(If you would like to read more like this, follow me here or my handbook Programming Without Anxiety, which will have a section about testing as well)

Takeaway: if you leave a codepath untested, you are willing to accept its functionality changing without notice in the future, and willing to bear the costs of debugging.

Collapse
juyn profile image
Xavier Dubois 🇫🇷 Author

Can't agree more, I like the driver analogy.
I'll just add that developers don't write unit tests until they work in a team.
Until then, you can have control on the base code. It will be fine, unless you forgot something, but, hey, that's not that often.
But, when a coworker mess with your code and change how it behave, unit test is the only way to keep controle

Collapse
mkimont profile image
Matt Kimek

I only disagree with diagram cosr arrow, should be opposite. Nowadays junior devs could write functional testing, api testing and integration testing. Unit testing is written by engineers and they time cost. Also time for unit testing is longer than writing tested code

Collapse
juyn profile image
Xavier Dubois 🇫🇷 Author

Unit testing should not be writing only by engineers.
Yes, they cost and they can take more time to write than the tested code. Can't disagree here.

But unit testing is the gate keeper. It ensure that, at a low level, your code is doing what is meant for.
And, if well wrote, it is a documentation for the next developer.

Others tests methods, like functional testing, integration testing etc are here to protect your production environment.
Is the data alright ? Are the performances alright ? Is the code style compliant?

They don't have the same purpose, otherwise we'd have stop writing them all already !

Collapse
mkimont profile image
Matt Kimek

Agree of course. None of our backend code goes live without full unit testing coverage. Sometimes loosing one number in code can cost one million in one day

Collapse
bootcode profile image
Robin Palotai

About unit test vs integration

I think we have false perception of the cost/time tradeoff of unit test vs integration test.

If we only look at time to write the test in the first place, and how fast it takes to execute, unit tests come ahead integration for sure.

But you have to add a lot more unit tests to cover the same component, compared to integration tests. Also, each refactor needs to alter a couple of unit tests as well - while integration tests operate on the outer interface, less likely to change, thus less likely to need modifications when refactoring.

Add to that my subjective that most hard bugs happen due to some mismatch on boundaries between systems, which is less captured by unit tests.

Not saying we shouldn't unit test, but its role is overrated vs integration tests.

About coverage

I agree that 100% coverage is just for the show. In my experience, you should aim for 60-70% coverage. To go above that, you have to add meaningless tests on getters and such.

But coverage in itself is meaningless. There was a story where after a big company mandate, coverage grew nicely. Until they found out devs copy-pasting dummy 100% tested classes just to push up the ratio of covered code.

I would advise this: you know which parts of the code are quirky. Use coverage as an assistant to ensure you have covered those well, for your own sake. Don't care about global coverage numbers.

Collapse
codemouse92 profile image
Jason C. McDonald

FIRST is a good acronym. "First" I've heard of it, too. Thanks.

Collapse
juancarlospaco profile image
Juan Carlos

TDD is poors man DbC. ;)