カイワレの大冒険 Third

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

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