カイワレの大冒険 Third

技術的なことや他愛もないことをたまに書いてます

Chefのテストスイーツを色々試してみた (1)chefspec, minitestなどによるテスト整備

エンジニアのみなさま、自動化は進めていますでしょうか。海外ドラマにハマってる@masudaKです。

すたじおなんとかさんが、以下のようなことを仰ってまして、

rbenvのcookbookでも作ろうかなと思ったのですが、折角なので前から触りたかったツールを色々使ってみました。その紹介記事であります。

記事書こうかなと思ってるうちにVagrantがクリティカルに進化してたりして追いつけてない(近いうちに動かなくなる)部分もあるかもしれませんし、Rubyも触りしかわかってないので、間違い等あったらご指摘お願いします。

ちなみにChefあまり触ったことがない人は、chef-soloとかでまず簡単なところからやってみるのがよいと思うので、そのへんは他の記事に任せます。また、Chefサーバのインストールは以下の記事に書いてますので、そちらでも(古いので、陳腐化してるかも…)。

blog.masudak.net


今回の目的はテストや試験的環境を作り、そのなかでテスト・レシピを書いて、リリースするということを念頭に置いています。
ただ、Chefと戯れ続けてると以下の様な課題も出てきて、そういうのもできるだけクリアできるように書いたつもりです。

  • 本番で実際レシピ流したら、エラー吐いて、途中までしか流れてない。
  • エラーも吐かずレシピ流れたけど、実は違う環境(ステージングとか)の設定が流れてて、間違ったバージョンとかのもの流してる(気づきにくい)。テストがない。
  • 違うプラットフォームとかに向けてレシピ書いてるうちにデグレって、昔書いた処理がうまく機能してないことに気づく。
  • 色々書いてるうちに、コードが複雑化してしまう。リファクタしたいのにそのまんま…

まぁ、実際触れると色々詰まるのですが、そんなこと言ってもしょうがないので、気楽にレシピ修正して、自動化進めましょうという話しを書いてみたいと思います。

まずは、構成

構成をまず考えましょう。といっても、最初から文字で羅列すると複雑なので、Githubにあげた構成を見て下さい。
github.com

-   cookbooks/
    -   cookbooks/rbenv
        -   cookbooks/rbenv/spec
        -   cookbooks/rbenv/files/default/tests/minitest

    -   cookbooks/Rakefile

-   feature/

今回重要なのは、以上となります。一つ一つ説明していきますが、実装にかけられる時間とかを考えると、省いたほうがいい部分もあったりするので、この記事のなかで、これはやってみようかなというものだけ是非やってみてください。あと、間違ってる箇所があれば、是非ご指摘をば。
では、説明していきましょう。

cookbooks/rbenv/spec

早速レシピを書きたい気持ちはありますが、最初は良くても、後々テストがなく困るということが割りとあります。パッケージAはバージョン固定で、バージョン1.3を入れるようにして書いていたのに、違うプラットフォーム・ディストリビューションだとバージョンが変わっていて、処理書き換えていたら、なぜか違うバージョンが入ってるとか…

無駄に全環境に流して確認しないといけないとか、まぁ後々困ることはよくあります。

ので、そういうのを避けるためにテストを書きましょう。ここではRspecの記述を利用しながら、レシピのテストができる、chefspecを使ってみます。2013年3月20日時点ではChef11系では動かないので、rspec実行のchefのバージョンは上げ過ぎないように気をつけましょう。

基本的な書き方はドキュメントに書いてありますので、熟読しておくのがよいでしょう。書き方はシンプルで、gitパッケージのバージョン1.7.1-3.el6_4.1が入ったかどうかの確認をしたければ以下の様に書きます。

it { expect(chef_run).to install_package_at_version "git","1.7.1-3.el6_4.1" }

んで、さらにテスト全体を書く方法を説明します。ドキュメントにあるサンプルを少しいじって説明すると、

# まずrequire
require 'chefspec'

# describeでテストしたいレシピ記述して、convergeで引数にレシピを指定したものをletでメモ化。要はchef_runオブジェクトをずっと使いまわせるようにする
describe 'my_new_cookbook::default' do
  let(:chef_run) { ChefSpec::ChefRunner.new.converge 'my_new_cookbook::default' }
  # itを使って、どうなるかの記述をして、そのなかで/var/lib/fooディレクトリが作られていることを確認します。
  it 'should do something' do
    expect(chef_run).to create_directory '/var/lib/foo'
  end
end

概要はこんな感じです。シンプルなレシピであれば、このようなテストを書くのはそこまで難しくないでしょう。

あとは、 「$ rspec -fd -c spec/default_spec.rb」 でrspecを実行して、確認してみましょう。

レシピがまだないので、エラーになりますが、とりあえずそのまま必要そうな処理を書いていきます。

んで、僕の環境だと、OSの違いとかあってもちゃんとカバーできるようにテストを書こうとして、以下のようになっていきました。

describe 'my_new_cookbook::default' do
  let(:chef_run) { ChefSpec::ChefRunner.new.converge 'my_new_cookbook::default' }

  describe 'When Centos' do
    it 'should do something' do
      expect(chef_run).to create_directory '/var/lib/foo'
    end
  end

  describe 'When Ubuntu' do
    it 'should do something' do
      expect(chef_run).to create_directory '/var/lib/foo'
    end
  end
end

これだと、対象とするプラットフォーム増えるたびに、「create_directory '/var/lib/foo'」って処理が増えていくわけですね。

レシピに10の処理が書いてあるなら、10×プラットフォームの数になるわけです。んで、抽象クラスを作って、そのなかにそれを継承したcentosクラスとかubuntuクラスを作ってみましたが、それでも処理が長いことには変わらない。。。

また、テストである性質というのもあってファイル名は変えたくないので、どうしてもrecipes/default.rbに対するテストはspec/default_spec.rbに書いておきたいわけです。

しかも、あまりに疎にして、ファイルが分離しすぎたら、具体的であるはずのテストも見難くなってしまう。と思っていたら、いいものありました。

shared_exaplesという共通処理をまとめられるものが用意されてるのですね。
なので、パッケージをインストールするレシピに対応するテストは以下の様な感じにしてみました。

shared_examples 'Package' do |package_name, package_version|
  context 'when install' do
    it { expect(chef_run).to install_package_at_version "#{package_name}","#{package_version}" }
  end
end
<中略>
    describe "Package git" do
      it_behaves_like 'Package', 'git', '1.7.1-3.el6_4.1'
    end

こうすると、describeで文脈が変わっても、処理は共通化させることができる。しかも引数渡せるという神ぐあいで、助けられました。
あとは、OSの違いをパラメータとして渡さないといけないので、node['platform']にsetしないといけない。
そういう場合は、以下のようにnode.automatic_attrsを使って、値を渡します。

    let(:chef_run) {
      runner = ChefSpec::ChefRunner.new()
      runner.node.automatic_attrs['platform'] = 'centos'
      runner.converge 'rbenv::default'
      runner
    }

んで、最終的な形になったのが、以下。
github.com

書いてみて、以下の印象を受けました。

  • 共通化もできるし、処理がわかりやすい。モジュールとかにすれば、もっと共通化できるのかな。
  • 書き方もよくわからなかったけど、リファクタのポイントをまとめてくれる資料がわりとあって、少しずつ見やすくできた(気がする)
  • ただ、gitリソースとかに対応してなくて、テストできない部分があった
  • 実際にレシピ流さなくてもrspecコマンドだけでテストできるので、非常に早い。気楽にテストできる。

という感じです。Rubyよくわかってないので、今もいい書き方になってるかは分かりませんが、レシピのテストは最低限はできるし、一度形を決めれば使いまわせるので、初心者には優しいかなぁと。ただ、最低限で全てカバーできるわけでは当然ないので、そこは注意が必要です。

cookbooks/rbenv/files/default/tests/minitest

レシピのテストとしては上述したchefspecの他にminitestというものがあります。

chefと連携させたものとしてはminitest-chef-handlerになります。

minitestってtest-kitchenと連携させて、オプスタでインスタンスの作成とともにレシピを流して、その後レシピのテストを行うってのが一般的なのかなと思ってましたが、そうでもないみたいですね。

ということで、オプスタを使わず、Vagrantでやってみましょう。

www.ryuzee.com

をもとにやってみると、「$ bundle exec kitchen test」で簡単にCent6.4とかのテスト環境を作ってテストできました。

Vagrantが使える環境なら、test-kichenを使ってやってみるのが一番楽かと思います。ただ、Cent6.4のVMはできたのですが、Ubuntuのとあるboxでは以下の様なエラーでテストできませんでした。

vagrant-1.0.7/lib/vagrant/action/vm/customize.rb:23:in `block in call': A customization command failed: (Vagrant::Errors::VMCustomizationFailed)

["storagectl", "37f52d42-6398-4c79-b79f-83bf49571509", "--name", "SATA Controller", "--hostiocache", "on"]

The following error was experienced:

VBoxManage: error: Could not find a storage controller named 'SATA Controller'
VBoxManage: error: Details: code VBOX_E_OBJECT_NOT_FOUND (0x80bb0001), component SessionMachine, interface IMachine, callee nsISupports
VBoxManage: error: Context: "GetStorageControllerByName(Bstr(pszCtl).raw(), ctl.asOutParam())" at line 1151 of file VBoxManageStorageController.cpp
VBoxManage: error: Couldn't find the controller with the name: 'SATA Controller'

こっちを直してもいいのですが、元々Vagrant環境はあったので、そっちで流せるようにしてみました。
やりかたは、自分がテストしたいcookbookがあるディレクトリに、

$ git clone https://github.com/btm/minitest-handler-cookbook.git
$ rm -rf minitest-handler-cookbook/.git

してみましょう。んで、必要であればgitignoreでレポジトリ管理下にいれないようにする。

あとは、ドキュメントにあるように"minitest-handler"をrun_listに加えるのですが、僕の場合はvagrant環境があるので、Vagrantfileに以下のように記述します。

   config.vm.provision :chef_solo do |chef|
     chef.cookbooks_path = "../masudaK-chef/cookbooks"
     chef.add_recipe "minitest-handler"
     chef.add_recipe "rbenv"
   end

これでminitestが「$ vagrant provision」のあとに走るようになります。

次に書き方を。
http://cookbooks/rbenv/files/default/tests/minitest/default_test.rb
を見て頂ければわかりますが、そこまでchefspecと乖離するほどの内容ではありません。

  include MiniTest::Chef::Assertions
  include MiniTest::Chef::Context
  include MiniTest::Chef::Resources

という記述をして、

  it 'installs git' do
    package('git').must_be_installed.with(:version, "1.7.1-3.el6_4.1")
  end

こんな感じで指定するだけです。ただ、chefspecと同じように、OSの違いとかを吸収したくなったわけです。
んで、書いてみたのがこちら。

require 'minitest/spec'

describe 'rbenv' do
  include MiniTest::Chef::Assertions
  include MiniTest::Chef::Context
  include MiniTest::Chef::Resources

  case "#{node['platform']}" # node attribute would be get inside it block...
  when "centos"

    it 'installs git' do
      package('git').must_be_installed.with(:version, "1.7.1-3.el6_4.1") # minitest-chef-handler
    end
    it "Directory dir" do
      file("/home/vagrant/.rbenv/plugins").must_exist.with(:owner, "vagrant").and(:group, "vagrant").and(:mode, "0755")
    end


  when "ubuntu"
    it 'installs git' do
      package('git').must_be_installed.with(:version, "1:1.7.10.4-1ubuntu1")
    end
    it "Directory dir" do
      file("/home/vagrant/.rbenv/plugins").must_exist.with(:owner, "vagrant").and(:group, "vagrant").and(:mode, "0755")
    end
  end
end

これ残念ながら、動きません…
コメントにも書いてありますが、nodeアトリビュートが使えるのはitブロックのなかだけなので、以下のようにしないといけないんですね。

    it 'installs git' do
      case "nodeほにゃらら"
      when "centos"
        package('git').must_be_installed.with(:version, "1.7.1-3.el6_4.1") # minitest-chef-handler
      when "ubuntu"
      ....
    end

これだと、当然冗長になってしまうわけです。ので、解決策募集中ですが、ファイル分ける以外でできるんだろうか…
ひとまず、使ってみた感想としては以下。

  • 書き方自体はrspec同様複雑ではない
  • 同時に複数のVMでテストできるので、ディレクトリの移動とかなくて楽
  • サーバに入って目で確認できるタイプ
  • がっつりレシピ流してからテストするので、ごっつい
  • nodeアトリビュートいじれない…

という感じでしょうか。現状chefspecのほうが僕にはあってる気がします。ただ、リソースの対応とかも増えてきたら、ちょっと考えるかもという状況です。いずれにしても、そこまで大きな書き方の差異はないですし、もっと柔軟にかけるかもなので、実は違うのかもしれません。

テスト・レシピを書く

とりあえず、テストは最低限書く環境ができました。rspecでもminitestでもどちらでもいいですが、テストを書きましょう。
項目としては、以下の様なイメージ。

  • rbenvを入れるための必要最低限環境を整える。パッケージいれたり、ディレクトリ作ったり。
  • rbenvをclone
  • ruby-buildをclone
  • .bashrcを書き換え
  • attributeに渡せるものは渡す。

という感じでしょうか。本当は.bashrcと.zshrcが選べたり、インストールのディレクトリを変更できたり、まぁ色々できたほうがいいのでしょうが、ひとまずシンプルに書くことを念頭に置きます。
したがって、テストは、

  • rbenvを入れるために必要なパッケージが入ってるかどうか(ex: git cloneするためにはgitパッケージが必要)
  • 必要なディレクトリが作られてるかどうか(ex: ruby-buildを入れるためには$HOME/.rbenv/pluginsディレクトリが必要)
  • .bashrcに必要な処理が入ってるか(ex: パスの追加とかが必要)

となるでしょう。 んで、書いたテストは前に示したように以下。
github.com

んで、それに合わせてレシピを書きます。 レシピは以下。
github.com

あとは、以下でテストします。

$ rspec -fd -c spec/default_spec.rb

出力は以下の様な感じ。

rbenv::default
  on a CentOS box
    Package git
      behaves like Package
        when install
          should install package at version "git" and "1.7.1-3.el6_4.1"
    Directory ruby-build
      behaves like Directory
        when create
          should create directory "/home/vagrant/.rbenv/plugins"
          should be owned by "vagrant" and "vagrant"
          should == 493
    Template .bashrc
      behaves like Template
        when create
          should create file with content "/home/vagrant/.bashrc" and "rbenv"
  on a Ubuntu box
    Package git
      behaves like Package
        when install
          should install package at version "git" and "1:1.7.10.4-1ubuntu1"
    Directory ruby-build
      behaves like Directory
        when create
          should create directory "/home/vagrant/.rbenv/plugins"
          should be owned by "vagrant" and "vagrant"
          should == 493
    Template .bashrc
      behaves like Template
        when create
          should create file with content "/home/vagrant/.bashrc" and "rbenv"

Finished in 0.07047 seconds
10 examples, 0 failures

できるだけ自然言語として意味が通るように作ってみましたが、どうだろうか…

この辺はbest practiceを紹介してるここ
betterspecs.org
とかを見ながら、直していくのがよいでしょう。

一応、minitestにも対応してるので、vagrant provisionすればテストできますが、ubuntuの場合は、以下のように当然怒られてしまうので、ここは課題だったりします。

Finished tests in 0.492544s, 8.1211 tests/s, 8.1211 assertions/s.

1) Failure:
test_0001_installs_git(rbenv) [/var/lib/gems/1.9.1/gems/minitest-chef-handler-0.6.8/lib/minitest-chef-handler/resources.rb:41]:
The package does not have the expected version.
Expected: "1.7.1-3.el6_4.1"
Actual: "1:1.7.10.4-1ubuntu1"

終わりに

ひとまず、ここまではいかにテストを書いていくかを紹介してみました。

どう環境作るか分からないうちは謎なことが多かったですが、分かってくるとそこまでテストも複雑ではありませんし、いろんなOSの空きサーバ用意するのも大変なので、テスト書いてみようかなという気になってます。そういう環境がないと、なんか謎のバージョンとか入っちゃったりしてるんですよね。

ということで、レシピのテストはあるといいよというテンションで今回は終わらせたいと思います。次回の記事では、ここまで書いたレシピやテストをいかにリファクタしていくのかを紹介してみたいと思います。

次回作はこちら。
blog.masudak.net

Chefのテストスイーツを色々試してみた (2)foodcritic, flay, reek, cucumberによるコード整備。

寒いの苦手なので早く温かい季節が来てほしいですが、花粉が怖くて、はざまの心境で揺れている@masudaKです。
前回の記事では、Chefのテストツールを紹介してみました。気楽にテスト。んで、テストは書くようになったものの、相変わらず自己流だったりするわけですね。それはまずいわけです。
ということで、この記事ではリファクタに焦点を当ててみたいと思います。

foodcritic

まずはlintということで、foodcriticを紹介したいと思います。
こちらの記事にも丁寧に紹介されているので、詳しい説明は避けますが、lintツールは気軽に入れられるので、入れてみましょう。勝手にチェックしてくれるんですから、嬉しい限り。

$ gem install foodcritic --no-ri --no-rdoc

とするかbundleで入れるだけ。 あとは、

$ foodcritic COOKBOOK_PATH

でチェックできます。

$ foodcritic --epic-fail any ./cookbooks/rbenv
FC019: Access node attributes in a consistent manner: /home/travis/build/masudaK/masudaK-chef/cookbooks/rbenv/recipes/default.rb:10

こんな感じで警告してくれるとともに、「--epic-fail any」をつけて、一つでも警告があれば終了ステータスで0以外を返すようにしてます。ただ、警告出してくれるのはいいのですが、テストとか書いてるうちにファイルが分散して、その上で一貫性がないとか言われると、わりと修正箇所見つけるの疲れるので、程々でいいかもしれません。

flay

次にflayを紹介します。レシピが増えていくと似たような処理も増えていくわけですね。変数にぶちこんだほうがいいのに文字列埋め込んでたり、ループしないで同じことやったり。そういうのを検出してくれるのがflayでございます。

$ gem install flay --no-ri --no-rdoc

で入れるかbundleで入れるだけ。 実行すると以下のような感じ。

$ flay  ./cookbooks/rbenv/
Total score (lower is better) = 178


1) Similar code found in :iter (mass = 100)
  ../masudaK-chef/cookbooks/rbenv/spec/default_spec.rb:30
  ../masudaK-chef/cookbooks/rbenv/spec/default_spec.rb:58

2) Similar code found in :iter (mass = 42)
  ../masudaK-chef/cookbooks/rbenv/spec/default_spec.rb:6
  ../masudaK-chef/cookbooks/rbenv/spec/default_spec.rb:20

3) Similar code found in :iter (mass = 36)
  ../masudaK-chef/cookbooks/rbenv/recipes/default.rb:20
  ../masudaK-chef/cookbooks/rbenv/recipes/default.rb:37

まぁ、言われた行見てると確かに似てます。そういう場合はデザパタ的なアプローチで切り出すなりしてもいいでしょうが、テストの可読性とかレシピ実行の順番考えると、どこまでやるかは悩ましいところだったりします(求:
ベターソリューション)

reek

人に誤解を与えるようなおかしな処理は入れたくないものです。ただ、自分で書いてると気づかなかったりする。そういうのをチェックしてくれるツールがreekです。ドキュメント見てると、引数を渡しすぎてたり、ブール値とるところでそれとは分かりづらい変数名使ってたり、リファクタにつながる部分を示してくれるツールのようです。
今まで説明してきたものと同様に以下のようにいれるか、

$ gem install flay --no-ri --no-rdoc

bundleで入れましょう。 実行結果はこんな感じ。

$ reek cookbooks/
cookbooks//minitest-handler/attributes/default.rb -- 0 warnings
cookbooks//minitest-handler/metadata.rb -- 0 warnings
cookbooks//minitest-handler/recipes/default.rb -- 0 warnings
cookbooks//rbenv/attributes/default.rb -- 0 warnings
cookbooks//rbenv/files/default/tests/minitest/default_test.rb -- 0 warnings
cookbooks//rbenv/metadata.rb -- 0 warnings
cookbooks//rbenv/recipes/default.rb -- 0 warnings
cookbooks//rbenv/spec/default_spec.rb -- 0 warnings
cookbooks//test_recipe/attributes/default.rb -- 0 warnings
cookbooks//test_recipe/metadata.rb -- 0 warnings
cookbooks//test_recipe/recipes/default.rb -- 0 warnings

レシピがRubyのスクリプトであると言っても、そこまでRubyRubyな感じではないので、検出はしにくいかもですね。ただ、テストとかでrubyらしい処理はすることもあるでしょうし、そういう意味では入れておいたもいいかもしれません。

番外編: cucumber

この記事ではlintツール、リファクタ系のものを紹介してきました。

前回の記事を含めここまで書いてきたことをやると、テスト書く、レシピ書く、リファクタするといった処理をループしていくことになるかと思います。ただ、一つできてない点がまだありまして、流したものが正しく動くかという保証はできていません。たとえば、レシピも書いて、本番に流したとします。そのサーバで問題なく、スクリプトは動くのでしょうか。デーモンは立ち上がるのでしょうか。


rbenvをインストールしてみたけど、rbenvコマンドはちゃんと叩けるのでしょうか。こんなことを早い段階で気づくにはRubyではどういう方法があるんだろうと思い、調べてみました。このような要求をかなえるには、cucumberというものを使います。nginxに関して説明された記事は既にあるようなので、rbenvでやってみた例を紹介してみたいと思います。


まず、cucumberとは何かを理解しましょう。記事だと以下が読みやすかったです。

前回の記事でrspec, minitestといったものでレシピのテストをしてきましたが、cucumberを理解するためには、rspec/minitestとcucumberとの違いを説明するのが早いと思います。

簡単にいえば、僕の理解では、rspecやminitestはホワイトボックス方式でして、内部でどういう処理をしてるのかを明らかにしてテストします。どう処理してるかをテストする。それに対してcucumberはその内部の処理は一切見ません。どう動くかだけをテストします。

なので、ブラックボックス方式。chefspecでテストしたのは、特定のディレクトリが作られてるか、特定のパッケージが入ってるかでした。

一方、cucumberはそんなことは見ません。その結果、特定のコマンド叩いて目的のことができてるかどうかを見ます。そして、その叩ける前提を明らかにして、どういうときにどうできるかを自然言語で表現します。日本語でも表現・記述できます。こんな違いがあります。


細かい説明だと小難しいので、例を示しましょう。以下の例は、実際にレシピを流す環境でまず実行しています。ただ、cucumber-chefというのがありまして、ドキュメント見てみる限りVagrantでできるっぽいので、Vagrantと連携させたほうが楽かもしれません。


まず、featureという拡張子のついたファイルを作ります。僕はcookbookとは独立させて管理させるようにしました。

features/rbenv.featureを見ていただくと分かるのですが、もろ日本語です。

シナリオ: '/home/vagrant/.rbenv/bin/rbenv'の実行可否を確認する
# Given
前提: '/home/vagrant/.rbenv/bin/rbenv'ができている
# When
もし: '/home/vagrant/.rbenv/bin/rbenv -v'を実行する
# then
ならば: 出力に以下が含まれている
""
rbenv
"""

特定のコマンドを叩いて、その出力に特定の文字列が入ってるだろうということを表しています。

とりあえず僕は「\$rbenv -v」で最低限文字列入ってくるだろうと思い、このシナリオを作ってみた次第でございます。


んで、実行してみる。インストール処理も入れると以下のような感じ。

$ gem install cucumber --no-ri --no-rdoc
$ cucumber features/rbenv.feature

そうすると、以下の様な出力が黄色で出てきます(一部)。

You can implement step definitions for undefined steps with these snippets:

前提(/^: '\/home\/vagrant\/\.rbenv\/bin\/rbenv'ができている$/) do
  pending # express the regexp above with the code you wish you had
end

ので、step_defenitionsを書きましょう。

$ cat features/step_defenitions/rbenv_step.rb
前提(/^: '\/home\/vagrant\/\.rbenv\/bin\/rbenv'ができている$/) do
  pending # express the regexp above with the code you wish you had
end

もし(/^: '\/home\/vagrant\/\.rbenv\/bin\/rbenv \-v'を実行する$/) do
  pending # express the regexp above with the code you wish you had
end

ならば(/^: 出力に以下が含まれている$/) do |string|
  pending # express the regexp above with the code you wish you had
end

それで再度cucumberコマンド実行しても、相変わらずskipされたり、pendingになっているかと思います。

これはfeatureでこういうことしたいと書いて、その確認のために実際こう確認しますとstep_defenitionsで書いても、「実際こう確認する」という具体的な処理が書かれていないからです。ので、具体的な処理を書いてみましょう。

それが以下。

$ cat features/step_defenitions/rbenv_step.rb
# encoding: UTF-8
require 'open3'
require 'rspec/expectations'

前提(/^: '(\/home\/vagrant\/\.rbenv\/bin\/rbenv)'ができている$/) do |file_path|
  File.exist?(file_path).should be_true
end

もし(/^: '(\/home\/vagrant\/\.rbenv\/bin\/rbenv \-v)'を実行する$/) do |cmd|
  Open3.popen3(cmd) do |stdin, stdout, stderr|
    @cmd_stdout =  stdout.readline
  end
end

ならば(/^: 出力に以下が含まれている$/) do |string|
  @cmd_stdout.should match('rbenv')
end

RubyRubyしてますが、実際処理を行うのでしょうがなかったりします。モックはできるかもですが、そこまでは調べきれてないのと、実際確認するためのテストなので、実環境で試したほうがいいでしょう。

ひとまず、実際ファイルが存在するかとか、出力を見ています。

これをrbenvが叩ける環境で実行すると、以下のように出力されます(※上記の'/home/vagrant/.rbenv/bin/rbenv'といったパスにインストールされてる場合に限る)。

$ cucumber
# language: ja
機能: rbenv
  rubyのバージョンを管理して、
  自分の好きなバージョンのrubyを使う機能を提供する

  @announce
  シナリオ: '/home/vagrant/.rbenv/bin/rbenv'の実行可否を確認する # features/rbenv.feature:8
    前提: '/home/vagrant/.rbenv/bin/rbenv'ができている     # features/step_defenitions/rbenv_step.rb:5
    もし: '/home/vagrant/.rbenv/bin/rbenv -v'を実行する   # features/step_defenitions/rbenv_step.rb:9
    ならば: 出力に以下が含まれている                              # features/step_defenitions/rbenv_step.rb:15
      """
      rbenv
      """

1 scenario (1 passed)
3 steps (3 passed)
0m0.027s

これで無事望んだ環境ができてます、という保証ができるわけです。
と書いてみましたが、使ってみると以下のような印象を受けました。

  • 確かに日本語で表現できるのは素敵。わかりやすい。自然言語も大事。
  • 「もし」を使わず、「前提」と「ならば」だけを使う例も多く、そういうテストがいいのかよくわかってない
  • ブラックボックス的なテストだけでなく、ホワイトボックス的なテストも書くのは大変
  • モックだとちゃんとした確認にならないし、実環境で叩くとなると、サーバが増えたら並列させたり工夫が必要になる。
  • 200を返すとか希望の環境ができてるかの確認は、基本監視でやってるから、監視スクリプトを叩くだけでいいんじゃないか
  • レシピと連携させるなら、_post_run.rbに確認したいコマンド書いて、default.rbとかでincludeさせれば、レシピ流した時点で例外返すかの確認はできる。chef-client -Wでドライ・ランしても多分確認できる。

という印象です。おそらく外注先に納品したり、セキュリティ的な定期チェックとか、そういう場合に仕様と照らしあわせて、質を担保するのに向いてて、レシピ流した環境の担保という意味での利用だとcucumberは少し敷居が高いかもと思いました。

タスク化&可視化

ここまで色々書いて来ましたが、テストやlintなどはタスク化できますので、まとめてしまいましょう。RubyだとRakeでできるので、以下のような処理を書いて実行させます。

cookbooks/Rakefileを実行すると以下の様な感じ。

$ rake -f ../masudaK-chef/cookbooks/Rakefile
-- rspec running --
/home/masuda_kenichi/.rbenv/versions/1.9.3-p392/bin/ruby -S rspec /home/masuda_kenichi/repos/masudaK-chef/cookbooks/rbenv/spec/default_spec.rb -c
..........

Finished in 0.09847 seconds
10 examples, 0 failures
-- foodcritic running --
foodcritic --epic-fail any /home/masuda_kenichi/repos/masudaK-chef/cookbooks/rbenv

foodcritic --epic-fail any /home/masuda_kenichi/repos/masudaK-chef/cookbooks/test_recipe

-- flay running --
/home/masuda_kenichi/repos/masudaK-chef/cookbooks
Total score (lower is better) = 100


1) Similar code found in :iter (mass = 100)
  /home/masuda_kenichi/repos/masudaK-chef/cookbooks/rbenv/spec/default_spec.rb:30
  /home/masuda_kenichi/repos/masudaK-chef/cookbooks/rbenv/spec/default_spec.rb:58


まとめて処理してくれるので楽ですね。今のところflayでは例外扱いはしてません。
あとは、公開するなら、Githubにpushしたときに同じようにテストしてくれたほうがよいでしょう。ということで、Travis CIを使います。

.travis.ymlというファイルをレポジトリトップにおいておいて、Travisでアカウントを作り、レポジトリ登録をしておきます。

ビルド結果はこんな感じ

んで、最後にREADME.mdに以下のように書いておくと、ステータスアイコンも表示してくれます(自分のレポジトリ名に置き換えてください)。

[![Build Status](https://secure.travis-ci.org/masudaK/masudaK-chef.png?branch=master)](http://travis-ci.org/masudaK/masudaK-chef)

これでステータスがパブリックになるので、ジョブがこけたままにはせずに、ちゃんと直そうという気にもなれるかと。社内向けのものであれば、JenkinsでRakefileを叩けば同じような結果得られるでしょうし、それぞれの環境に合わせるのがいいかと。

終わりに

ここまでずらずらと書いてみましたが、環境を作ってしまえば、レシピ書いてるあいだに確認できることもかなり増えますし、何より自分で確認しなくていいのが楽で仕方なかったりします。色々書いたので、そのなかで取捨選択して頂くのがいいかと思います。あと、自分だったらこうするとか、こういう方法もあるとかあれば、是非教えて頂きたく。

最近はChefもかなり注目されてるような印象を受けますし、国内の資料も色々増えてきた気がしますので、もっと普及して、より情報が増えるといいなぁと思います。また新しいこと試したら、追記できればと思います。