Search code examples
rubyrspecthorvcr

Testing a Thor script with rspec and vcr


I built a Thor script that connects to an HTTP API to perform some very simple actions. I've coded tests for the backend but the Thor script is basically untested, which is quite suboptimal.

My first approach was to capture the output of the commands itself and write test against such output, the resulting tests are unsurprisingly slow.

expect(`bin/script foo`).to eq('bar')

I then tried to use both webmock and vcr but using this approach none of these frameworks are invoked, even if I mock the exact request the mock is unused, most probably because both webmock and vcr are unable to hook into the thor script.

Has anybody found a nice solution for this? Invoking the Thor script directly (Thorclass.action('bar')) would be enough for my taste, but I haven't found a way to do it. Any suggestion? Thanks in advance.


Solution

  • Thor is a wrapper

    • I tend to see Rake, Thor and friends as another interface to your code
    • I keep my Thor/Rake code as tiny as possible
    • All production code is kept in a standard Ruby class
    • That means unit testing via VCR becomes dead easy
    • Also allows you to reuse your production code in another interface: e.g. a Rails controller

    Example

    Thor wrapper

    bin/seed

    #!/usr/bin/env ruby
    require "thor"
    
    class Seed < Thor
      desc "budgets", "Seeds budgets"
      def budgets
        puts 'Seeding currencies...'
        SeedBudgets.new.call
        puts 'Done.'
      end
    end
    
    Seed.start
    

    For more details on command line Thor see this excellent walkthrough

    Production code

    lib/services/seed_budgets.rb

    class SeedBudgets
      def initialize
        # I find an initialize helpful for injecting dependencies
      end
    
      def call
        # Code goes here
      end
    end
    

    Unit tests

    test/services/seed_budgets_test.rb

    require 'minitest/autorun'
    require 'vcr'
    
    VCR.configure do |config|
      config.cassette_library_dir = 'fixtures/vcr_cassettes'
      config.hook_into :webmock
    end
    
    class SeedBudgetsTest < Minitest::Test
      def test_seeds_one_budget
        VCR.use_cassette('one_budget_from_api') do
          SeedBudgets.new.call
          assert_equal 1, Budget.count
        end
      end
    end
    

    That will allow you to decouple the command line interface from the actual code.

    Then Thor becomes a very thin wrapper around your actual code.

    Feel free to post more detailed code and I can help more. :)