Isolated Integration Tests in Shell

As I’ve been building out During, we have a handful of microservices powering things like syncing, importing, some single-purpose serving of heavy data, and so on.

If you aren’t familiar, a microservice is just a fancy word that means “let’s spread our business objects all OVER the fucking place!”.

Anyway, my microservices are all, well, really really micro. And because I’m definitely not above being an idiot, I’ve rewritten one of my web services a number of times, starting with a folder inside of Rails, then extracted out into Ruby, then Crystal, then Go, and now back to Ruby. It’s not a huge time sink, and to be honest, much of this was just for shits and giggles to try out new approaches.

I’ve since moved all these services back to Ruby. I’m really good at Ruby these days, and it’s really easy to scale Ruby to handle all of my users. Granted, the app hasn’t even launched into beta yet, so it turns out it’s also really easy to scale when you have no users. In fact, virtually everything about software development is easier when you have no users. Aside from the making money part, that is (but that hasn’t stopped Silicon Valley much either).

Anyway, I digress. Something I’ve been really enjoying as of late is specifically using shell to test these services.

POSIX shell, more like UBIQUITOUSIX shell amirite

I’ve been writing my tests for these services in shell script, and it’s been pretty great. For one, the infrastructure is obviously easy. Shell is already on all of my AWS instances, any CI service I may use, and on my local machine. Don’t have to install anything, don’t have to run any Docker instances (though that certainly doesn’t hurt).

The most important aspect, though, is that my tests are now isolated and independent from any future language testing I may use. I can bounce around between languages and frameworks, and I don’t have to change any of the tests. This is really important because if, for example, your v1 has a subtle bug that your tests were fine with, you can rewrite the whole v2 service and your tests will fail if you accidentally corrected the bug. That means that your API you expose to the rest of your services won’t break, and you can prepare the rest of your services to fix the bug at your leisure instead of being surprised by it in production.

The tooling for shell testing is pretty decent these days, too. For years I’ve used my buddy Blake Mizerany’s roundup, a lovely tiny testing tool for shell. Recently I’ve been using Sam Stephenson’s bats, which has been steadily growing into a really active community. (lol, around a shell testing tool, who would have thought?) Most of my shell tests now look like this one, in bats:

@test "Responds with events within the given timespan" {
  run curl "$URL$url_params" --silent -H "Authorization: Bearer:$bearer"

  assert_output --partial "Test Event 0"
  assert_output --partial "Test Event 2"
  refute_output --partial "Test Event 5"
  refute_output --partial "No location data"
  refute_output --partial "Not included in the date span"

Tests are pretty simple and easy to reason about. Basically just a curl and you check the output. Done.

Integration rules everything around me

One last quick point: these microservices are very-small to fairly-small, and I can get by with ignoring writing any other tests entirely. Writing integration-only, full-stack tests is really interesting, but people get really religious about whether this is the next best idea ever or the worst idea in the world. For what it’s worth, GitHub’s Gist was happily running in production with zero unit tests whatsoever for years. I’m somewhat up in the air about the practice overall; I go back and forth for sure. There’s plenty of other posts on the topic you can read if you’re interested.

But I will say that in these cases, wowza, what a breath of fresh air. Our tests are portable, and we don’t have to write any new tests if I ever rewrite the service. I just have to conform to my shell-based tests.