A Case For Unit Tests
3 min read11-Feb-21
This week, I fixed a bug. Actually, as a point of fact, I fixed a few bugs. But one in particular stands out to me, because it was a bug in code I wrote (!!).
I hate writing buggy code. Duh. But this one in particular really set me on tilt!
A few months ago, I had to write a few Quartz jobs that could call a service method to query our database for some records based on their expiration dates, then email concerned parties about those records. Each job would perform the same query, but look at a different date range, and send a different message accordingly.
To solve this, I put together a handy helper function that accepted a date range and performed the query. It might have looked something like:
1Map fetchClientsWithExpiringRecords(2 Integer daysUntilExpiry,3 Integer dateRange,4 RecordStatus status5 ) {6 List<Record> allRecords =7 Record.findAllByExpirationDateBetweenAndStatus(8 new Date() + daysUntilExpiry,9 new Date() + daysUntilExpiry + dateRange,10 status11 )1213 // more code to parse `allRecords` for the relevant info14 // and grab the clients associated with the records1516 return clientList17 }
I then iterated over the results and fired off an email to each client on the list.
Simple, right?
Well, yeah! It actually is really straightforward. However, this code has the same room for error all code does: some big dummy (like yours truly) can come along and fat-finger a number provided to the method.
So when I want to send the email for anything expiring between 60-66 days from now and invoke this method like so:
1fetchClientsWithExpiringRecords(60, 66, RecordStatus.Foo)
Can you guess what happens?
I'll wait.
.
.
.
Yep, my fat fingers slipping all over my keyboard introduced a bug.
That method invocation should read:
1fetchClientsWithExpiringRecords(60, 6, RecordStatus.Foo)
Now, you might ask yourself why I care more about this than a normal bug. Again, I write buggy code all the time. I try my best, I write integration tests, I listen on code reviews... And yet, bugs still happen.
But you know what I didn't do when writing this code? I didn't unit test it.
I wrote API tests to check the Quartz job's behavior and manually tested it with Postman. I checked all my interactions from 20,000 feet up by hitting the endpoint we expose for our AWS Quartz runner. But I didn't unit test it.
There's no real excuse, and I'm not saying this would definitely have been caught with a few I/O unit tests. But the point remains without unit testing, I set myself up for failure right off the rip.
My PR touched one line of code and made one character change. I deleted a 6
from the method invocation in question. It also included a 152 lines of new unit tests that took me a grand total of two minutes to spin up, once I had the first one working.
Overcorrection? Maybe. Expensive? Absolutely not. Unit tests are cheeeeeap. They take next to no time to write once you have a reasonable paradigm in place to manage dependency injection and interaction with layers like Hibernate/GORM.
Tip for the day: don't be like me. Write your unit tests. CYA. Don't let fat fingers hurt your production code.