Photo by Sincerely Media on Unsplash
Let's break some eggs
Unit and Integration testing rules to break and rules to live by
We developers all write our unit tests that exercise the code and assert results properly. But not always, not when someone is asking for the feature or fix and we jump right into changing code without adding a test that reveals the bug/feature first.
I am going to tell you a few secrets about testing your code, I am going to tell you it is not time-consuming or boring and it can be readable! In fact, I use tests to show how my APIs should be used.
The rules of the game
Your test code can avoid most of the rules your production code has to follow. Because it has a different objective: prove your code works and that all the error cases are identified.
It does not need to pass static code reviews or use design patterns.
General test code rules
- readability over terseness
- duplication over calling utilities that hide important details
- test only factory methods with sensible defaults over tedious repetitive builders
- K.I.S.S (Keep It Simple Stupid) over D.R.Y (Don't Repeat Yourself)
- "cheat & lie 🤥" but make a point
- click through another file to understand a test intention is a NO-NO
- scroll to another part of the file to see assertions is another NO-NO
ℹ️ One test method/function reads from start to end with no jumping around
ℹ️ Production code only called in tests should be removed
Unit tests
- UT mocks everything other than a single class/method being tested
- UT looks at the class/method being tested, it is the subject
- UT never interacts with any I/O boundary
- UT doesn't test generated code (that is why we like generated code😉)
- UT is fast (milliseconds)
- UT can be data-driven when cyclomatic complexity is high and returns are simple enough
- UT asserts every decision fork
- UT covers invalid input or out of bounds input to clamp down that operation accepted parameters
- UT targets complexity coverage (line coverage will follow, but is irrelevant)
- UT never has framework context injected dependencies
- UT must run in parallel, speed is essential
Integration tests
- IT mocks the system boundaries (REST endpoints, File systems, Databases, Streams, and other I/O), here we can cheat to make it faster
- IT will load a context and may inject boundary mocks
- IT covers a single flow at a time. The flow is the subject!
- IT test name should more or less explain the flow start and assertions
- IT may look like it has one or more subjects: a class/method that receives the input, sometimes the results can only be inspected with another class/method call
- IT is not so fast (from one to sometimes several seconds)
- IT usually starts at one of the mocked boundaries and ends at another boundary
- IT covers the most essential and/or critical behaviors to clamp down those behaviors, one at a time.
- IT assertions would reasonably map to use case or sometimes acceptance criteria
- IT may check critical processing time performance indicators if there are required metrics, even while cheating on the I/O (rarely happens on first delivery, just for very intensive processes)
- IT every boundary may fail and we may want to check the behavior of that situation
- IT seldom runs in parallel
End-to-End tests
While not the focus here, let's point out the difference:
- End-to-End tests do not mock anything but may have artificial stimuli to trigger functionality, should cover even fewer functionality but have a focus on checking if all entry points and outputs means are covered
When you get your hands on an existing project
- it is OK to delete a test that does not make sense (version control is there for that)
- it is OK to rewrite a deleted test so that it makes sense
- always find the subject before writing your test
- don't trust a test that never failed
In the Java ☕ world
- Lombok Builder:
@Builder(toBuilder=true)
is your friend - Try Spock (test code is not a world of pain)
- easy learning curve
- super productive to write on
- fast to run
- so much easier to read
- no more reflection yoga to touch your code privates 😜 (daddy joke achievement)
- if you are using Gradle:
gradle -t test
allows for continuously running tests in the background
Unit tests
- check Jacoco report for missing tests, not based on lines but on complexity
- check test reports for the execution time and make sure they are not slow
- make sure high cyclomatic complexity is refactored or quieted with annotation as the last resource
Integration tests
- Find a Mock or an in-memory replacement for your data persistence layer, never use the real deal
- For instance, PostgreSQL replaced with h2 in memory: [h2database.com/html/features.html#compatibi..
- Avoid using Wiremock or other API calling mechanisms, if you can as it slows everything down by a lot. at first
Later I will post about some killer pseudo-random generated beans that save so much time!