I’m working on a few little command-line utilities at the moment, written in Ruby. They all use a web service somewhere along the way. This raised an interesting question: How do I test these things?
Obviously, when testing individual components, I can use something like the excellent VCR library. And for testing the CLI, I’m using Cucumber and Aruba. As a bit of background, Aruba extends Cucumber by letting you invoke console commands, and capture the stdout for matching in your feature files. This presented a small problem. VCR works by hooking itself into your HTTP library at a really low level, but Aruba launches my gem in a whole new process, which won’t be under the control of my test code.
tl;dr VCR can’t magically hook into classes of a separate Ruby process.
Luckily, Aruba has a trick up its sleeve. If the thing you’re testing is written in Ruby, it can be launched in-process, which means my VCR config would work! It’s actually pretty straight-forward. Let’s write a simple Thor app that fetches random advice, tested by Aruba and VCR.
The full source for this exercise is on Github.
The setup is simple enough. Use Bundler to create your new gem:
Then add Thor, VCR, WebMock, Cucumber and Aruba to your dependencies by adding this to your gemspec file:
1 2 3 4 5
And create the basic Cucumber/VCR layout:
Now we write our feature:
1 2 3 4 5 6 7 8 9 10 11
Run Cucumber (using Bundler)
Missing steps, of course! So now we do our environment setup. Create a file called ‘features/support/env.rb’ that looks like this:
And run bundle exec cucumber again. Now we’re getting somewhere. Your test can’t actually find anything to run. So let’s create it. Create ‘bin/advice’ with the following content.
1 2 3
Now create the actual CLI class in ‘lib/advice/cli.rb’
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
Now we have a failing test. Problem is, it will always fail, because we get a random different result every time. So let’s introduce VCR. Edit your env.rb file to look like this:
1 2 3 4 5 6 7 8 9 10 11 12 13
And add some support for VCR in our feature, by adding the @vcr tag:
1 2 3 4 5 6 7 8 9 10 11 12
What we’re doing in the env.rb file, is configuring VCR to detect when the feature has the @vcr tag, and use the feature name as a cassette name. If you’re unfamiliar with VCR, it records HTTP responses in YAML files called ‘cassettes’ (it can use other formats, but this isn’t a VCR tutorial so I’ll not go into that).
If we were to run Cucumber now, it’s fairly reasonable to expect the fetched web response to be recorded. But of course, it won’t, because VCR is hooking into the HTTP library in the Ruby process that’s running our tests, but the web calls are being made in a process spawned by Aruba. Go ahead and run it, nothing gets captured.
This is where our magic comes in. Add the following to the top of your env.rb:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
to the bottom.
What we’re doing here is, telling Aruba to spawn our custom class above, which we use to bootstrap our Advice CLI. The contents of our binary launcher have been duplicated here, which is a mild annoyance, but that should be doing virtually nothing anyway. We hook up the Aruba-provided stdin and stdout, and we’re good to go. Our test will fail for the very last time. Obviously, we need to know what it is we’ve captured. So in your feature, replace “some advice” with whatever was actually returned last time. In my case, “Don’t be afraid to ask questions.”
There. Every time you run Cucumber, the client will behave exactly as in production, except the web call is intercepted, and the same response is given, keeping your tests reliable.