I think about Test-Driven Development frequently nowadays. I use it for work everyday. And I’m trying my darnedest to use it in all of my personal projects too.
It’s easy to end up writing tests that are either over-engineered or tests that don’t cover edge cases enough. For the most part, we just want to be able to sleep at night knowing that our software works how it’s intended to.
At one point, I thought that every callable function should be tested. Nowadays, I actually think that in most cases, only the outer-most interfaces need to be tested, since those are ultimately where the request will be received and response sent. Of course, for complex systems where data is transformed in numerous stages, creating tests for smaller components is very beneficial.
There’s also a good reason to focus more on testing interfaces rather than implementation: you have the freedom to do whatever you want to the guts of your app as long as it provides the right output for a given input. If you choose to refactor, there would be minimal to no modifications required for the tests.
With the nitty gritty out the way, this brings me to the topic of CRUD interfaces. Most software uses CRUD. Whether it’s a social platform, a news website or an ecommerce store, we need to be able to Create, Read, Update and Delete objects.
Over the years, I’ve tried to figure out the simplest way to write tests that handle a CRUD API. At this point, I think I have something that makes sense. Let’s say we have a video sharing platform with user-generated content like YouTube. Here’s how I would write a test for CRUDing videos. For simplicity, I would break it down into a singular test and a plural test:
- Get a video by a random ID: should return nothing
- Create a video
- Get the video by ID: should matches initial values
- Update the video by ID
- Get the video by ID: should match updated values
- Delete the video by ID
- Get the video by ID: should return nothing
- Query list of videos: should return empty array
- Create a video
- Query list of videos: should return array containing the 1 video
- Create a video
- Query list of videos: should return array containing the 2 videos
And those two tests should handle basic CRUD for a single collection/table. The single test handles operations performed on a single item. Sometimes an “update” might return the updated version of the object. Either way, we should explicitly get the object afterwards to make sure it’s updated. The plural test handles querying a list. It might seem redundant to query the list for zero, one and two items, but I have a good reason for it:
- I want to ensure that the system can handle an empty list
- I want to ensure that the system can return one item
- I want to ensure that the system can return multiple items
It’s possible for #1 to fail if some code always assumes items will be present. It’s possible for #2 to fail if an object cannot be created or the query simply doesn’t return anything. And it’s possible for #3 to fail if the system was only designed to store a single object rather than an array of objects. I can safely assume that if two objects are queried successfully, then a thousand objects will be queried successfully.
So I’m fairly happy with this approach to TDD + CRUD. I’m also mulling over more complex cases, like a nested object schema, but I don’t think that case requires any special philosophy… yet.