Aruba gemでCLIのテストを支援する

## <i class='fa fa-question-circle'></i> Aruba とは？
ArubaはCucumber、RSpec、Minitestのような人気のあるTDD/BDDフレームワークで
コマンドラインアプリケーションのテストを簡単で楽しいものにする拡張です。

* [cucumber/aruba - GitHub](https://github.com/cucumber/aruba)

## <i class='fa fa-smile-o'></i> 特徴
* どんな言語で実装されたコマンドラインツールでもテスト可能
    * テスト自体はRubyで書くが、テスト対象は Python の CLI ツールでも Golang の CLI ツールでもよい
* ファイルシステムやプロセス環境をヘルパーによって操作できる
    * 例えば read でファイルを読み込みできる
    * 例えば run で外部コマンドを実行し、その結果を have_output matcher などで検証できる
* ファイルシステムやプロセス環境はテストのたびにリセットされるので、 leaking state がない
    * 例えばテスト中に作成されたファイルはテスト終了後には消えている
* コミュ二ティーサポートが手厚い
* [ドキュメント](https://github.com/cucumber/aruba/tree/master/features) にあるとおりに動作することが期待できる

## <i class='fa fa-search-minus'></i> フォーカス
`gem install <your-gem>` のようなインストールや、ツールのデプロイ自体については
Arubaのフォーカス外とする。

## <i class='fa fa-flask'></i> サンプル
fizzbuzz を実行する CLI ツールを作成します。

### <i class='fa fa-check'></i> 準備
サンプルプロジェクトを準備します

* プロジェクトを作成

```sh
$ bundle gem -t rspec --exe fizzbuzz
```

* gemspec に thor と aruba を追加する

```ruby:fizzbuzz.gemspec
# 略
  spec.add_runtime_dependency 'thor'

  spec.add_development_dependency "aruba"
# 略
```

* 依存パッケージをインストールする

```sh
$ bundle install --path vendor/bundle
```

* FizzBuzzの実装 lib/fizzbuzz.rb

```ruby:lib/fizzbuzz.rb
require 'fizzbuzz/version'
require 'cli'

# FizzBuzz
#
# FizzBuzzの計算を行う
module Fizzbuzz
  module_function

  def fizzbuzz(limit)
    limit_number = Integer(limit)
    (0..limit_number).map do |num|
      if (num % 15).zero? then 'FizzBuzz'
      elsif (num % 5).zero? then 'Buzz'
      elsif (num % 3).zero? then 'Fizz'
      else num.to_s
      end
    end
  end
end
```

* FizzBuzzの実装 lib/cli.rb

```ruby:lib/cli.rb
require 'thor'

# Fizzbuzz::Models
module Fizzbuzz
  # CLI
  #
  # FizzBuzzのコマンドライン
  class CLI < Thor
    class_option :help, type: :boolean, aliases: '-h', desc: 'help message.'
    class_option :version, type: :boolean, desc: 'version'
    class_option :debug, type: :boolean, aliases: '-d', desc: 'debug mode'

    desc 'fizzbuzz', 'Get fizzbuzz result from limit number'
    def fizzbuzz(limit)
      print Fizzbuzz.fizzbuzz(limit).join(',')
      exit
    rescue => e
      output_error_if_debug_mode(e)
      exit(false)
    end

    desc 'version', 'version'
    def version
      puts Fizzbuzz::VERSION
    end

    private

    def output_error_if_debug_mode(e)
      return unless options[:debug]
      STDERR.puts(e.message)
      STDERR.puts(e.backtrace)
    end
  end
end
```

* FizzBuzzの実装 exe/fizzbuzz

```ruby:exe/fizzbuzz
#!/usr/bin/env ruby

require "fizzbuzz"

Fizzbuzz::CLI.start(ARGV)
```

* FizzBuzzのテストの実装 spec/fizzbuzz_spec.rb

```ruby:spec/fizzbuzz_spec.rb
require 'spec_helper'

describe Fizzbuzz do
  it 'has a version number' do
    expect(Fizzbuzz::VERSION).not_to be nil
  end

  context '.fizzbuzz' do
    it "正常系" do
      expect(%w(FizzBuzz 1 2 Fizz 4 Buzz Fizz 7 8 Fizz Buzz 11 Fizz 13 14 FizzBuzz)).to eq(Fizzbuzz.fizzbuzz("15"))
    end

    it "数値以外の入力" do
      expect { Fizzbuzz.fizzbuzz("a") }.to raise_error(ArgumentError)
    end
  end
end
```

### <i class='fa fa-wrench'></i> Aruba RSpecのセットアップ
ArubaはCucumber, RSpec, Minitest に対応しています。  
今回はRSpec版を利用します。

```sh
$ bundle exec aruba init --test-framework rspec
      append  spec/spec_helper.rb
   identical  spec/support/aruba.rb
      append  Gemfile
```

* Gemfile に aruba が追加されます

```ruby:Gemfile
source 'https://rubygems.org'

gemspec
gem 'aruba', '~> 0.14.2'
```

* spec/support/aruba.rb が追加されます

```ruby:spec/support/aruba.rb
require 'aruba/rspec'
```

* spec/spec_helper.rb に以下が追記されます

(require 'fizzbuzz'だけ手動で追記しました)

```ruby:spec/spec_helper.rb
$LOAD_PATH.unshift File.expand_path('../../lib', __FILE__)
require 'fizzbuzz'

if RUBY_VERSION < '1.9.3'
  ::Dir.glob(::File.expand_path('../support/*.rb', __FILE__)).each { |f| require File.join(File.dirname(f), File.basename(f, '.rb')) }
  ::Dir.glob(::File.expand_path('../support/**/*.rb', __FILE__)).each { |f| require File.join(File.dirname(f), File.basename(f, '.rb')) }
else
  ::Dir.glob(::File.expand_path('../support/*.rb', __FILE__)).each { |f| require_relative f }
  ::Dir.glob(::File.expand_path('../support/**/*.rb', __FILE__)).each { |f| require_relative f }
end
```

* spec/cli_spec.rb

以下のコードが Aruba のテストコードです

```ruby:spec/cli_spec.rb
require 'spec_helper'

RSpec.describe 'fizzbuzz command', type: :aruba do
  context 'version option' do
    before(:each) { run_command ('fizzbuzz v') }
    it { expect(last_command_started).to be_successfully_executed }
    it { expect(last_command_started).to have_output("0.1.0") }
  end

  context 'help option' do
    expected = <<~EXPECTED
    Commands:
      fizzbuzz fizzbuzz        # Get fizzbuzz result from limit number
      fizzbuzz help [COMMAND]  # Describe available commands or one specific command
      fizzbuzz version         # version

    Options:
      -h, [--help], [--no-help]        # help message.
          [--version], [--no-version]  # version
      -d, [--debug], [--no-debug]      # debug mode
EXPECTED

    before(:each) { run_command ('fizzbuzz help') }
    it { expect(last_command_started).to be_successfully_executed }
    it { expect(last_command_started).to have_output(expected) }
  end

  context 'fizzbuzz subcommand' do
    expected = %w(FizzBuzz 1 2 Fizz 4 Buzz Fizz 7 8 Fizz Buzz 11 Fizz 13 14 FizzBuzz).join(',')
    before(:each) { run_command ('fizzbuzz fizzbuzz 15') }
    it { expect(last_command_started).to be_successfully_executed }
    it { expect(last_command_started).to have_output(expected) }
  end

  context 'fizzbuzz subcommand invalid args' do
    before(:each) { run_command ('fizzbuzz fizzbuzz a') }
    it { expect(last_command_started).not_to be_successfully_executed }
  end
end
```

* `to be_successfully_executed`
    * status 0 で終了していることを確認
* `not_to be_successfully_executed`
    * status 0 以外で終了していることを確認
* `to have_output(contents)`
    * 出力が contents であることを確認。正規表現も使用可能

### <i class='fa fa-terminal'></i> 実行
```sh
$ bundle exec rspec spec/cli_spec.rb

fizzbuzz command
  version option
    should be successfully executed
    should have output: "0.1.0"
  help option
    should be successfully executed
    should have output: "Commands:\n  fizzbuzz fizzbuzz        # Get fizzbuzz result from limit number\n  fizzbuzz help [COMM...e.\n      [--version], [--no-version]  # version\n  -d, [--debug], [--no-debug]      # debug mode\n"
  fizzbuzz subcommand
    should be successfully executed
    should have output: "FizzBuzz,1,2,Fizz,4,Buzz,Fizz,7,8,Fizz,Buzz,11,Fizz,13,14,FizzBuzz"
  fizzbuzz subcommand invalid args
    should not be successfully executed

Finished in 3.1 seconds (files took 0.2151 seconds to load)
7 examples, 0 failures
```

## <i class='fa fa-book'></i> 参考
Rubyの静的解析ツール RubyCritic で Aruba + Cucumber を利用しています。

* [whitesmith/rubycritic - GitHub](https://github.com/whitesmith/rubycritic)

## <i class='fa fa-github'></i> tbpgr/fizzbuzz
今回実装したサンプルプログラムは以下にupしてあります

* [tbpgr/fizzbuzz - GitHub](https://github.com/tbpgr/fizzbuzz)

## <i class="fa fa-book" style="font-size:1em;"></i> 外部資料
* [aruba docs - relish](http://www.relishapp.com/cucumber/aruba/docs)
* [cucumber/aruba - GitHub](https://github.com/cucumber/aruba)