Thorを使ってコマンドラインツールを作成してテストを通す

毎回作り方を忘れてしまうので、次のためにまとめます。

gemひながたの生成

$ bundle gem samplegem -b

これでファイルが一式出てきます。--testとしなくてもrspecのテスト雛形も含まれてます。

$ cd samplegem
$ ls -1a
.
..
.git
.gitignore
.rspec
.travis.yml
Gemfile
LICENSE.txt
README.md
Rakefile
bin
exe
lib
samplegem.gemspec
spec

こんな状態です。

最初にsamplegem.gemspecを編集しないと動きませんが、いかにもデフォルトなところだけでいいです。取り急ぎ動かすだけなら適当にdummyとかで埋めれば動きます。

実行ファイルは、exe/samplegemです。中身は

#!/usr/bin/env ruby

require "samplegem"

だけで処理が何も入っていません。

とりあえず、末尾にputs 'hoge'を追記して以下のようにします。

#!/usr/bin/env ruby

require "samplegem"

puts 'hoge'

bundle installします。

$ bundle install --path vendor/bundle
Fetching gem metadata from https://rubygems.org/..........
Fetching gem metadata from https://rubygems.org/.
Resolving dependencies...
Fetching rake 10.5.0
Installing rake 10.5.0
Using bundler 1.16.0
Fetching diff-lcs 1.3
Installing diff-lcs 1.3
Fetching rspec-support 3.7.0
Installing rspec-support 3.7.0
Fetching rspec-core 3.7.1
Installing rspec-core 3.7.1
Fetching rspec-expectations 3.7.0
Installing rspec-expectations 3.7.0
Fetching rspec-mocks 3.7.0
Installing rspec-mocks 3.7.0
Fetching rspec 3.7.0
Installing rspec 3.7.0
Using samplegem 0.1.0 from source at `.`
Bundle complete! 4 Gemfile dependencies, 9 gems now installed.
Bundled gems are installed into `./vendor/bundle`

あと実行権限がついてないのでchmod +xししてあげると、

$ chmod +x exe/samplegem
$ bundle exec exe/samplegem
hoge

動きます。


thorの導入

コマンドラインツールとして動かせるように、thorを入れます。

samplegem.gemspecに、

spec.add_dependency "thor"

を追記します。そして再度bundle install

これでthorがインストールされ、コマンドラインツールとしてGemを動かせるようになります。

とりあえず、

$ bundle exec exe/samplegem hello ほげほげ
こんにちは、ほげほげさん

と、helloの後に名前を渡すと、あいさつを返すところを目指します。

以下のようにファイルを作成・編集します。

exe/samplegem

#!/usr/bin/env ruby

require "samplegem"

Samplegem::Command.start(ARGV)

lib/samplegem/command.rb

require 'samplegem'
require 'thor'
require 'samplegem/hello'

module Samplegem
  class Command < Thor
  desc 'hello [Name]', 'あいさつを返すよ'
    def hello(name)
      hello = Samplegem::Hello.new
      hello.say(name)
    end
  end
end

lib/samplegem.rb

require "samplegem/version"
require "samplegem/command"

module Samplegem
  # Your code goes here...
end

require "samplegem/command"を追加

lib/samplegem/hello.rb

require 'samplegem'

module Samplegem
  class Hello
    def say(name)
      puts "こんにちは、#{name}さん"
    end
  end
end

動きました。

$ bundle exec exe/samplegem hello ほげほげ
こんにちは、ほげほげさん

テストがしたい

雛形に含まれるテストのサンプルはコメントアウトして、

spec/samplegem_spec.rb

RSpec.describe Samplegem do
  it "has a version number" do
    expect(Samplegem::VERSION).not_to be nil
  end

#  it "does something useful" do
#    expect(false).to eq(true)
#  end
end

テスト実行

$ bundle exec rake spec

Samplegem
  has a version number

Finished in 0.00164 seconds (files took 0.14576 seconds to load)
1 example, 0 failures

「バージョン番号があること」というデフォルトのサンプルテストのみが通る状態です。

ここに、samplegem hello ふがふがに対して「こんにちは、ふがふがさん」が返ることを確認するようにテストを追加していきます。


Aruba Gemの導入

https://qiita.com/miya0001/items/f0d94ae144c85483354e https://qiita.com/yohm/items/e7f7203d42c2c4e653d8

いくつか記事見ましたが、どうもコマンドラインのテストがうまくできず、

[Aruba gemでCLIのテストを支援する](https://qiita.com/tbpgr/items/41730edcdb07bb5b59ad “Aruba gemでCLIのテストを支援する”)

でようやくテストが動いたので、Arubaを使った手順で進めます。

samplegem.gemspecに、

spec.add_development_dependency "aruba"

を追記します。そして再度bundle install

$ bundle install
Fetching gem metadata from https://rubygems.org/.........
Fetching gem metadata from https://rubygems.org/.
Resolving dependencies...
Using rake 10.5.0
Fetching ffi 1.9.18
Installing ffi 1.9.18 with native extensions
Fetching childprocess 0.8.0
Installing childprocess 0.8.0
Fetching contracts 0.16.0
Installing contracts 0.16.0
Fetching builder 3.2.3
Installing builder 3.2.3
Fetching backports 3.11.0
Installing backports 3.11.0
Fetching cucumber-tag_expressions 1.1.1
Installing cucumber-tag_expressions 1.1.1
Fetching gherkin 5.0.0
Installing gherkin 5.0.0
Fetching cucumber-core 3.1.0
Installing cucumber-core 3.1.0
Fetching cucumber-expressions 5.0.13
Installing cucumber-expressions 5.0.13
Fetching cucumber-wire 0.0.1
Installing cucumber-wire 0.0.1
Using diff-lcs 1.3
Fetching multi_json 1.13.1
Installing multi_json 1.13.1
Fetching multi_test 0.1.2
Installing multi_test 0.1.2
Fetching cucumber 3.1.0
Installing cucumber 3.1.0
Using rspec-support 3.7.0
Using rspec-expectations 3.7.0
Using thor 0.20.0
Fetching aruba 0.14.3
Installing aruba 0.14.3
Using bundler 1.16.0
Using rspec-core 3.7.1
Using rspec-mocks 3.7.0
Using rspec 3.7.0
Using samplegem 0.1.0 from source at `.`
Bundle complete! 5 Gemfile dependencies, 24 gems now installed.
Bundled gems are installed into `./vendor/bundle`
Post-install message from aruba:
Use on ruby 1.8.7
* Make sure you add something like that to your `Gemfile`. Otherwise you will
  get cucumber > 2 and this will fail on ruby 1.8.7

  gem 'cucumber', '~> 1.3.20'

With aruba >= 1.0 there will be breaking changes. Make sure to read https://github.com/cucumber/aruba/blob/master/History.md for 1.0.0

ruby 1.8.7の注意が書いてありますが関係ないので無視します。

initを実行します。

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

spec_helper.rbにsupportディレクトリを読みに行くように追記がされます。

spec/spec_helper.rb(追記箇所のみ)

...

$LOAD_PATH.unshift File.expand_path('../../lib', __FILE__)

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

support/aruba.rbが作成されます。

spec/support/aruba.rb

require 'aruba/rspec'

Gemfileにも何か追記されました。

source "https://rubygems.org"

git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }

# Specify your gem's dependencies in samplegem.gemspec
gemspec
gem 'aruba', '~> 0.14.3'

これでテストが書けます。

spec/samplegem_spec.rb(追記)

...
RSpec.describe 'samplegem command', type: :aruba do
  context 'hello option' do
    before(:each) { run('samplegem hello ふがふが') }
    it { expect(last_command_started).to be_successfully_executed }
    it { expect(last_command_started).to have_output("こんにちは、ふがふがさん") }
  end
end

テストします。

$ bundle exec rake spec
Samplegem
  has a version number

samplegem command
  hello option
    should be successfully executed
    should have output: "こんにちは、ふがふがさん"

Finished in 0.65393 seconds (files took 0.19792 seconds to load)
3 examples, 0 failures

通りました。