How to write good unit tests? There is no one golden answer. Today I will tray to show you a few unit testing tips&tricks.
What’s the Unit?
What is a unit test? it’s testing process of one unit. What is one unit ? A unit is the smallest part of your code you can test. What’s more important UNIT is not a singular file
Unit is the smallest part of your code you can test.
TIP 1 - don’t run IO operations in units
This is really important rule. You shouldn’t execute any IO operation (database connections, api calls, filesystem writing etc.) in Unit tests - they should be fast&quick&light. If we are talking about CI pipelines I would really recommend, to separate Unit Tests from Integration Tests, and run unit first. This way you will achieve fail-fast approach. Unit-tests would fail much faster than Integration-tests.
TIP 2 - do not mock everything
It’s that simple. You just don’t have to mock everything. Let me explain it by an example (Spock example, but there will be JS later):
The above example illustrate the “mock everything” approach. You need to answer the question: What would you like to test?
The good answerer is: business logic
.
Do you really need 3 tests classes to verify above logic? You need to decide. It would be good-enough to create an instance of each of these dependencies, and then test everything in each business scenario.
Much better In the end it means that you wouldn’t create separate tests files for Dep1, Dep2 because those classes are already tested. Again, I’m not telling you that “mock” is something evil -> but just think if you really need to mock everything in your test.
Okay, so now let’s answer to following question: Is there any kind of object which must has own “test” file? Sure, but you need to ‘feel’ it. There is no golden rule. In java, you can assume that each package-private class is a good candidate to omit a separate test file.
TIP 3 - focus on business logic
This is the most important tip. I can paraphrase this to: avoid verify/toHaveBeenCalled. I know that’s sad, but it’s an excellent tip.
Verify&toHaveBeenCalled are like a everyday bread, it’s so common. I think the reason is that it’s much simpler to write tests in this way.
What verifying methods executions number is actually doing is putting a thousand kilograms of concrete at top of your code.
Let’s go to example:
This is only one simple test, and you can say there is nothing wrong with this one. Can you tell me what have been tested here? You are not checking any results, just mock execution.
A bit better approach could be to avoid void
as a return type, and return something like this;
(1) - return business logic result
(2) - now we are checking "one record has been updated"
not if "repository updateRecord method has been executed once"
What has been changed? We are focus on business logic now.
You can tell that in the second example we are not executing any business logic because of mocks … I can give a simple answer: in the first example neither. What has been checked in the first version is MOCK OBJECT EXECUTION nothing more. If you are not drunk or something, and you are not going through a codebase with a scissors… the code that executes mocked logic should be there
TIP 4 - use right tools for testing things
It’s really common to see developers who would like to make 100% test coverage at all costs.
He put verify
or toHaveBeenCalled
on EVERYTHING. In the end you’d have many tests that tests nothing, and puts tons of concrete at top of your codebase.
Wait wait wait, are you trying to tell me that verify
is evil!? Nope - I’m not saying it’s evil. I’m saying that it’s easily overused.
You need always look at the business - are business logic care about “application execute method A in mock B”? Not at all! Only if there is a business behind it.
We can get Angular example.
Let’s have a deeper look into example. We have a service FooService
, and this service executes some HTTP request against API.
The first idea could be to pass a mock through constructor, and use toHaveBeenCalled
. Why not?
Because there is a better way to do it :) there is a right tool for it in Angular, called HttpTestingModule
. With its help you can trace all http requests, and check what’s really happening.