Sean’s Obsessions

Sean Walberg’s blog

Test Driven Infrastructure

In software, test driven development happens when you write an automated test that proves what you are about to write is correct, you write the code to make the test pass, then you move on to the next thing. Even if you don’t follow that strict order (e.g. write your code, then write a test), the fact that there’s a durable test that you can run later to prove the system still works is very valuable. All the tests together give you a suite of tools to help prove that you have done the right thing and that regressions haven’t happened.

What about the infrastructure world? We’ve always had some variant of “can you ping it now?”, or some high level Nagios tests. But there’s still some value to knowing that your test was good – if you make a change and then test, how can you be sure your test is good? If you ran the same test first you’d know it failed, then you could make your change. And then there’s the regression suite. A suite of tests that may be too expensive to run every 5 minutes through Nagios but are great to run to verify your change didn’t break anything.

Enter the Bash Automated Test System - a Bash based test suite. It’s a thin wrapper around the commands that you’d normally run in a script but if you follow the conventions you get some easy to use helpers and an easy to interpret output.

As an example, I needed to configure an nginx web server to perform a complicated series of redirects based on the user agent and link. I had a list of “if this then that” type instructions from the developer but had to translate them into a set of cURL commands. Once I had that it was simple to translate them into a BATS test that I could use to prove the system was working as requested and ideally share with my team so they could verify correctness if they made changes.

share_link tests
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
#!/usr/bin/env bats

@test "root" {
  run curl http://example.com
  [[ $output =~ "doctype html" ]]
}

@test "mobile redirects to share" {
  run curl -H "User-Agent: this is an iphone" -i -k http://app.example.com/shareapp/65ac7f12-ac2e-43f4-8b09-b3359137f36c
  [[ $output =~ "302 Found" ]]
  [[ $output =~ "Location: http://app.example.com/share/65ac7f12-ac2e-43f4-8b09-b3359137f36c" ]]
}

@test "mobile redirects to share and keeps query string" {
  run curl -H "User-Agent: this is an iphone" -i -k http://app.example.com/shareapp/65ac7f12-ac2e-43f4-8b09-b3359137f36c?a=b
  [[ $output =~ "302 Found" ]]
  [[ $output =~ "Location: http://app.example.com/share/65ac7f12-ac2e-43f4-8b09-b3359137f36c?a=b" ]]
}

@test "desktop redirects to play" {
  run curl -H "User-Agent: dunno bob" -i -k http://app.example.com/shareapp/65ac7f12-ac2e-43f4-8b09-b3359137f36c
  [[ $output =~ "302 Found" ]]
  [[ $output =~ "Location: http://app.example.com/play/65ac7f12-ac2e-43f4-8b09-b3359137f36c" ]]
}

@test "desktop redirects to play and keeps query string" {
  run curl -H "User-Agent: dunno bob" -i -k http://app.example.com/shareapp/65ac7f12-ac2e-43f4-8b09-b3359137f36c?a=b
  [[ $output =~ "302 Found" ]]
  [[ $output =~ "Location: http://app.example.com/play/65ac7f12-ac2e-43f4-8b09-b3359137f36c?a=b" ]]
}

@test "bots redirect to main site" {
  run curl -H "User-Agent: facebookexternalhit" -i -k http://app.example.com/shareapp/65ac7f12-ac2e-43f4-8b09-b3359137f36c
  [[ $output =~ "302 Found" ]]
  [[ $output =~ "Location: http://www.example.com/app/social?id=65ac7f12-ac2e-43f4-8b09-b3359137f36c" ]]
}

@test "bots redirect to main site and keeps query string" {
  run curl -H "User-Agent: facebookexternalhit" -i -k http://app.example.com/shareapp/65ac7f12-ac2e-43f4-8b09-b3359137f36c?a=b
  [[ $output =~ "302 Found" ]]
  [[ $output =~ "Location: http://www.example.com/app/social?id=65ac7f12-ac2e-43f4-8b09-b3359137f36c&a=b" ]]
}

And running the tests with one mistake in the configuration:

$ bats ~/Downloads/share_link.bats
 ✓ root
 ✓ mobile redirects to share
 ✗ mobile redirects to share and keeps query string
   (in test file /Users/sean/Downloads/share_link.bats, line 17)
     `[[ $output =~ "301 Found" ]]' failed
 ✓ desktop redirects to play
 ✓ desktop redirects to play and keeps query string
 ✓ bots redirect to ndc
 ✓ bots redirect to ndc and keeps query string

 7 tests, 1 failure

With the tests in place it’s more clear when the configurations are correct.

As a bonus, if you use Test Kitchen for your Chef recipes, you can include BATS style tests that will be run. So if this configuration is in Chef (which it was) I can have my CI system run these tests if the cookbook changes (which I don’t yet).

Comments

I’m trying something new here. Talk to me on Twitter with the button above, please.