I don’t know why, but I’ve been fascinated with unit testing for years. Yes, you don’t have to point out how nerdy that is, I know, trust me.
I remember thinking that it’s a great idea to be able to check that my code works, all the time and I don’t have to manually do anything … manually …. gosh I really dislike this word. Well, not the word per se, but the activity of spending my precious time, repeatedly doing the same uncreative work.
Am I faster than a computer? No
Am I more accurate? No
Do I have nothing better to do? I do actually
So then, why should I do these activities?
Of course, there is more to it than this, it’s not pure laziness that drives this, it’s more to do with the desire to build quality code, have less bugs and be able to refactor without the fear that everything ends up in the black hole of never again working code. This name itself is scary …. BNAWC, something to do with claws …. yuck.
So, let’s see, what exactly are unit tests? How do we define them and what are their characteristics? I find this is a great place to start as it makes us question, no not life you nerd, but what we are actually doing as part of the coding activity.
A unit test is a piece of code which exercises a small part of our application’s code.
This is a very generic definition and doesn’t help anyone, so let’s move on to characteristics …
- do not touch anything outside of the small piece of code they are testing
- can be run quickly, repeatedly, without setup and without any specific order
- are very simple and do not require complex setup, including much mocking
- do not have deep knowledge of the code they test.
Let’s look at all of these, one by one.
- Unit tests do not touch anything outside of the small piece of code they are testing.
Unit tests do not touch the file system, a database, an API or anything else like this.
All these things require a number of things and take some time. You will need a connection string, an API key, the API might even be down, there might be an issue with your database or the API is not even yours but it is a third party one. Does this mean that if this API is down, you cannot complete your tests?
If you have one test and it takes 2 seconds to run, that doesn’t sound so bad. What happens when you have a thousand though?
Are you happy to wait 600 seconds? That’s 10 minutes by the way.
This doesn’t sound so bad until you realize that the whole point of unit tests is that you run them all the time and you even hook them up to your CI/CD pipeline. Now one run takes an extra 10 minutes or more, which is really bad for everyone. You want quick feedback so people don’t even think about this extra test run.
2. can be run quickly, repeatedly, without setup and without any specific order
We’ve already seen why they need to run quickly and without setup.
What about running repeatedly?
This means they don’t affect the state of the system, by calling the code under test, you’re not setting global things, or anything else like that which influences other parts of the code and thus other tests. This means keep your code in check, so that no surprises happen when any piece of code is run.
You don’t want to hunt through your entire codebase trying to find out why the system is in a particular state. This leads into the next one:
3. unit tests are very simple and do not require complex setup, including much mocking
I have seen huge tests doing complex setup over hundreds of lines of code, I’ve seen mocking taken to extreme. Note I did not say don’t do mocking, I said don’t do much mocking, huge distinction.
SOLID encourages you to think about decoupling things, it encourages you to code against abstractions, not hard coded dependencies.
This is good advice, but you can also fall into the trap of doing too much decoupling.
Then, you write a test and you discover that you have to mock dependency A, then dependency B,which now needs dependency C and you keep going down the rabbit hole until you end up with a highly complex setup.
Once you’re done with the test, it looks like a three-headed hydra, big, ugly and scary. All these things make your tests very expensive to maintain, this is why I always advocate for simplicity. If your test looks too ugly, then look at the code because it is not as testable as you thought it was.
4. unit tests do not have deep knowledge of the code they test.
If your code knows too much about the code what happens is that when the code changes ( through refactoring for example ) then your tests will have to change too.
If the behaviour of the code does not change, neither should the tests
This is what makes unit testing worthwhile and cheap to maintain. This is what makes refactoring possible and safe, you have the harness to make sure you don’t break anything so go ahead and refactor to your heart’s content. If the tests are still green then everything is good.
It doesn’t matter how the code changes, your tests, once written have no reason to change, unless the behaviour of the code needs to change. In this case you change the tests, you now have failing tests, you fix the code until tests are green and move on.
If the tests have deep knowledge of the code, then you will have a maintenance nightmare on your hands, the tests will be too expensive to fix every time there is a change, will end up being commented out and the whole thing has been a giant waste of time. This is where it all goes.
What can we do to prevent this from happening?
My advice would be to aim for a functional coding style as much as possible. If a method that you want to test has a number of dependencies passed as parameters for example, then change it and pass data instead.
You can prepare the data first, then pass it to a method, this makes testing very easy because now you don’t have to worry about mocking anything, you just pass the data in whatever format you need.
This is the functional way. Everything a method needs is inside the method, no dependencies at all.
Random thoughts and questions
Is everything suitable for unit testing? I am going to say no.
What is suitable for unit testing? Everything which has a very well defined behaviour, things like an algorithm, formulas, code which changes something in a well defined way. Essentially anything which has business value and can be well defined, without any doubts.
What is not suitable for unit testing? Things which only make sense to developers, a DI setup, things which check how many times a method is called, stuff like that.
Imagine this scenario:
You go and talk to a business analyst ….
hey man, here is this test where I am checking that I am calling a method called RabbitIsJumpingUpAndDownOnOneFoot exactly twice and no less, no more. What do you think about the business value of this method?
If he looks at you like you are a martian with a green head then the answer is no, you should not have that thing. I cannot call it a unit test because it is not.
I have seen some pretty weird things categorized as unit tests, some examples:
When I call this controller I get this specific view back.
What exactly are you testing? That the MVC framework works? Leave that to the framework, that’s not a unit test you should be having.
When my application starts up I verify that all dependencies are injected correctly, with a unit test
nope. you want to check everything is hooked up correctly? Write a few integration tests which rely on the dependencies being correctly injected, then you know everything is set up correctly.
I am testing that this method is called once
what’s the value of this? what happens when the code changes and that method is not called anymore? how do you even know what that method does?
I am testing this code which depends on random values created by this random generator, so I am running this test one thousand times to see what happens when specific values are generated.
Why ? Take the randomness out of your method, generate the thing outside of it, pass the generated thing as a parameter, now you can test your method with specific values, without having to run it like crazy, hoping it will eventually hit the values you want.
I can probably continue like this for quite a while, but hopefully the idea is clear by now. A bit of thought when writing code makes our life a lot easier when it comes to maintaining or changing the code.
The biggest fallacy I’ve ever heard is that unit testing adds whatever percentage ( typically 30 % ) to the delivery time. Let me stop you right there and tell you that is completely wrong.
Why? Writing testable code is no different than writing non-testable code. It’s the same thing as far as delivery is concerned. Adding proper unit tests is very quick, no problem there.
We, as developers debug our code a lot. If we don’t have unit tests then we have to either write some separate console application or keep going through the UI to test and see what happens.
Going through the UI is very time consuming and let’s face it, no one ever manually tests every possible scenario in every little corner of their application. You’d be ecstatic if even the happy path is tested at all.
Automated unit tests ensure that they run quickly and they all run. You basically save time, because you can cut down a lot on your manual testing time ( for all developers and testers in the team ), while making sure you test a lot more than via the manual way. So, no, adding unit tests does not add anything to your delivery, it just makes you deliver better quality, a lot quicker IF you have decent unit tests. Big IF there …