George McIntosh

I do something...with software...

VCR and Aruba

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:

1
$ bundle gem advice

Then add Thor, VCR, WebMock, Cucumber and Aruba to your dependencies by adding this to your gemspec file:

1
2
3
4
5
  spec.add_development_dependency "vcr"
  spec.add_development_dependency "webmock"
  spec.add_development_dependency "cucumber"
  spec.add_development_dependency "aruba"
  spec.add_dependency "thor"

And create the basic Cucumber/VCR layout:

1
$ mkdir -p features/support

Now we write our feature:

1
2
3
4
5
6
7
8
9
10
11
Feature: The advice CLI
  In order to get some random advice
  As a CLI user
  I want to request advice

  Scenario: get some advice
    When I run `advice please`
    Then the output should contain:
     """
      some advice
     """  

Run Cucumber (using Bundler)

1
$ bundle exec cucumber

Missing steps, of course! So now we do our environment setup. Create a file called ‘features/support/env.rb’ that looks like this:

1
require 'aruba/cucumber'

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
#!/usr/bin/env ruby
require 'advice'
Advice::CLI.start(ARGV)

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
require 'thor'
require 'open-uri'
require 'json'

module Advice
  class CLI < Thor

    desc "please", "fetches random advice from the Internet"
    def please
     the_advice = JSON.parse( open('http://api.adviceslip.com/advice').string)
     puts the_advice['slip']['advice']
    end

  end
end

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
require 'aruba/cucumber'
require 'vcr'
require 'webmock'

VCR.cucumber_tags do |t|
  t.tag  '@vcr', :use_scenario_name => true
end

VCR.configure do |c|
  c.hook_into :webmock
  c.cassette_library_dir     = 'features/cassettes'
  c.default_cassette_options = { :record => :new_episodes }
end

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
Feature: The advice CLI
  In order to get some random advice
  As a CLI user
  I want to request advice

  @vcr
  Scenario: get some advice
    When I run `advice please`
    Then the output should contain:
     """
      some advice
     """

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
require 'advice'

then add

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class VcrFriendlyMain
  def initialize(argv, stdin, stdout, stderr, kernel)
    @argv, @stdin, @stdout, @stderr, @kernel = argv, stdin, stdout, stderr, kernel
  end

  def execute!
    $stdin = @stdin
    $stdout = @stdout
    Advice::CLI.start(@argv)
  end
end

Before('@vcr') do
  Aruba::InProcess.main_class = VcrFriendlyMain
  Aruba.process = Aruba::InProcess
end

After('@vcr') do
  Aruba.process = Aruba::SpawnProcess
  VCR.eject_cassette
  $stdin = STDIN
  $stdout = STDOUT
end

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.

Have fun!

Comments