weblog of key_amb

主にIT関連の技術メモ

RSpec で example の外で定義したローカル変数を使うのはアリか?

※9/3 @jnchito さんのコメントを受けて追記しました。

RSpec で example の外で定義したローカル変数を使う

テストコードの例

こういうの:

outside_var = :outside_var # (1)

describe :top_scope do
  top_scope_var = :top_scope_var # (2)

  it :example_in_top_scope do
    expect(outside_var).to be_truthy
    expect(top_scope_var).to be_truthy
  end

  context :second_scope do
    second_scope_var = :second_scope_var # (3)

    it :example_in_second_scope do
      expect(outside_var).to be_truthy
      expect(top_scope_var).to be_truthy
      expect(second_scope_var).to be_truthy
    end
  end
end

これまでのところ、他人が書いたコードでこういう書き方を見たことはなかった。
外側で定義した変数を使う場合、 let を使うか before ブロックでインスタンス変数に代入するのがふつうだろう。

が、上のような書き方でも、ふつうに期待通りに動く。

特徴・用途

ローカル変数を使う場合には、let を使って宣言する場合や beforeインスタンス変数を使う場合と比較すると、以下のような特徴がある。

  • 変数定義は1回だけ実行される 〜 before :context 相当
    • letbefore :each では、example ごとに実行される
    • describe ブロックが評価され、RSpec の ExampleGroup が作られる時点で実行されているようだ
  • スコープが有効
    • 上の例で、 :top_scope から second_scope_var は見えない
    • let で宣言する変数や before で定義するインスタンス変数と同様
  • let 同様、typo に気づける。インスタンス変数だとエラーにならないので、気づかない恐れが有る(参考記事を参照)

RSpec の内部構造に踏み込んで深く追ったことはないのだけど、挙動を確認した限り、上のようだった。

ローカル変数のユースケースとしては、一々毎回実行・評価しなくていいような性質のもので、テスト中に状態が変わらないものに使えそうだと考えている。リファクタ用途としても使えそうだ。

「アリ」なのか、「ナシ」なのか?

自分自身としては、何ヶ月か前 fireap *1などを作っていた頃は、RSpec の作法をよく知らなかったので、ローカル変数を多用していた。

しかし、後になって RSpec について色々調べている内に、このような書き方を全く見かけないことに気がついた。
とはいえ、逆に「こういうのは駄目だ」とはっきり書いてある記事やドキュメントもなかった。

…ので、こういう書き方がアリなのかナシなのかというのが、数カ月ぐらい気になっていた。

で、最近初めて自分以外の人でそういう書き方をしている人を見かけたので、気になり度がしきい値を突破して、とある Rubyist が集うチャンネルで聞いてみた。


私「RSpec 詳しい人いますかね? example の外でローカル変数定義して使うのってアリなのかなぁというのが気になってます。こんなの↑(上のようなコード)」

S氏「1. rspecdsl で書くと、ほらわかりやすいでしょ?というのが走りなので、dsl を使わなくなったらなぜ rspec を使うのか、という話になりそう。
 2. let はオプションで thread-safe にしたり thread non-safe にしたり切り替えられるはず。
 3. let は それぞれの it の中で、それぞれ呼ばれるのでちょっと動きが違う。
 ぱっと思いつくやつ↑」

O氏「

describe "parameterized test" do
  [ 5, 10, 15 ].each do |i|
   it { expect(fizzbuzz(i)).to eq("buzz") }
  end
end

 みたいなパラメタライズドテストを rspec-parameterized みたいなのをかかなかったら普通にそうなってるので、特に気にしないでやればいいんじゃないかな?」


確かに、そういう書き方をしたこともあった。
ってか、 rspec-parameterized 知らなかった。

2016/9/2 現時点の結論

…というわけで、今のところ自分の中では「アリ」ということになっている。

ただ、テスト実行中に変数の状態が変わったりしたら、なんかまずそうなので、定数にしたり freeze した方が安全かも、と思ったり思わなかったり。

どうなんでしょうね。

(9/3 追記)

@jnchito さんが、本件についての見解・回答を示すブログを書かれています。

結論だけ書くと「興味深いけど積極的に使うのは NG」という感じです。
興味のある方は是非、上の記事もご覧ください。

9/3 追記:ローカル変数が使えそうな例

なんと、参考記事等でおなじみの @jnchito さんからコメントをいただきました。
正直、Qiita のコメントで聞いてみようかと思ったぐらいなので、僥倖でした。ブログ書いてよかった。

「絶対ローカル変数が良い、というサンプルコードがあれば教えてください」

とのこと。

「絶対」ということはないかな、と思いますが、「まあ、こういうケースならアリかな(?)」と個人的に思う例を3つほど書いておきます。

(1) 長いメソッド等のリファクタ系

特に、実行結果が毎回変わらないものがよさそうです。

describe MyReservationsController do
  in_sale = Reservation.statuses[:in_sale]
  reserved = Reservation.statuses[:reserved]
  cancelled = Reservation.statuses[:cancelled]
  out_of_sale = Reservation.statuses[:out_of_sale]

  let(:client) { FactoryGirl.create(:client) }
  let(:parsed_response_body) { JSON.parse(response.body) }
  
  describe '#index' do
    context '販売中のもの'
      context 'ログイン前' do
        it '予約が見つからない' do
          get :index, status: in_sale, count: 10
          expect(parsed_response_body['Reservations']).to be nil
        end
      end
      context 'ログイン後' do
        before do
          client.login
        end
        it '販売中の予約が取得できる' do
          get :index, status: in_sale, count: 10
          parsed_response_body['Reservations'].each do |r|
            expect(r['status'])
          end
        end
      end
      context 'メンテナンス中' do
        before do
          expect(MyService).to receive(:maintenance?).and_return(true)
        end
        it '予約が見つからない' do
          get :index, status: in_sale, count: 10
          expect(parsed_response_body['Reservations']).to be nil
        end
      end
    end
  end
end

もちろん、 let で書けないことはないし、↑の例だと get ... をまとめて subject 化するのもアリと思います。

(2) セットアップに時間のかかるオブジェクトを1回だけ生成し、結果を利用したい

下はプリミティブな DB のコネクション・ハンドラを自作しているような前提です。

conn = MyDatabaseConnector.setup!

describe MyTransactionApp do
  context 'normal' do
    it do
      Foo.query(conn, type: :normal)
      :
    end
  end
  context 'strange' do
    it do
      Foo.query(conn, type: :strange)
      :
    end
  end
end

RSpec の流儀に合いそうな別解としては、下のように書き換えられることもあるかもしれません:

describe MyTransactionApp do
  let(:conn) { MyDatabaseConnector.connection }

  before :all do
    MyDatabaseConnector.setup!
  end
  
  :
end

あるいは、 helper module にできるケースもあるかもしれません。

(3) 時間がかかるわけでもないが、1回だけセットアップして結果を利用したい

target_path = Foo.method_to_get_path(some_arguments, ...)
tmp_path = Pathname.new(Dir.tmp_dir) + 'foo'

describe MyApp do
  before :all do
    # 対象ファイルがあれば退避
    if File.readable?(target_path) do
      FileUtils.move(target_path, tmp_path)
    end
  end

  after :all do
    # 対象ファイルを復帰
    if File.readable?(tmp_path) do
      FileUtils.move(tmp_path, target_path)
    end
  end
  
  context 'A' do
    before do
      File.write(target_path, 'A')
    end
    after do
      FileUtils.remove(target_path)
    end
    
    it 'target_path の内容に依存するテスト' do
      :
    end
  end

  context 'B' do
    before do
      File.write(target_path, 'B')
    end
    after do
      FileUtils.remove(target_path)
    end
    
    it 'target_path の内容に依存するテスト' do
      :
    end
  end
end

この場合の別解としては、 target_path を helper method にしたり、 tmp_path をインスタンス変数にして before :all でセットする、というやり方もできそうです。

参考

enhancd を改修して "cd -" や "cd .." の挙動を変えずに使えるようにした

Bash で enhancd を導入することにした

cd の履歴を peco で移動したい。

シェル環境で peco を使いだした人なら、きっとそう思うことがあるでしょう。

zsh だと、cdr と組み合わせることで実現できます*1
下の記事あたりを参考に設定するといいでしょう。

さて、bash の場合、最近は b4b4r07/enhancd を使う人が多いのではないかなと思います。
いくつか作者の @b4b4r07 さんが記事を書いてます。

この enhancd は Bash の小枝集 で紹介されている cdhist.sh をベースに作られたそうです。zsh と fish にも対応しているそうです。
自分も一瞬、何か自作しようかと思ったのですが、enhancd が便利そうだったので、まずは使ってみることにしました。

使い方は README の通りですが、ソースを clone して . init.sh するだけです。
これで cd が enhancd の関数に alias されます。

色々機能がありますが、詳しくは上に挙げた Qiita の記事がわかりやすいだろうと思います。

とりあえず、自分にとって最低限必要だったのは cd - で cd の履歴を検索して移動できるようになることだけでした。
他に、 cd .. すると peco が起動してディレクトリを上方検索してくれました。

Pull Request に至る経緯と改修後の enhancd の設定方法

最初「おぉ。これは便利」と思って使っていたのですが、しばらく経ってから cd -cd .. を打つと、以前の挙動と違って peco の選択画面になるので、戸惑うことに気づきました。
慣れようと思ったこともありましたが、無理でした。

元々、それ以前に peco を使う機能にはキーボード・ショートカットを割り当てることにしていた*2ので、そちらに寄せたいと思いました。
また、以前は運用エンジニアとして色んなサーバに出入りしていたこともあって、あまりデフォルトの挙動を変えたくないという気持ちもありました。

というわけで、 cd -cd .. のデフォルトの挙動を変えずに、ディレクトリの履歴検索や上方検索を別の特別な引数に割り当てられるようにする Pull Request を送りました。

この PR を送ったのは2ヶ月ほど前のことでしたが、一昨日ようやくマージしてもらえました。

~/.bashrc の設定例は次のようになります。

ENHANCD_HYPHEN_ARG="-ls"
ENHANCD_DOT_ARG="-up"
. path/to/enhancd/init.sh
bind -x '"\C-ur": cd -ls'

ENHANCD_HYPHEN_ARGENHANCD_DOT_ARG が追加した設定用の変数です。

上のように設定すると、 cd -lsディレクトリの履歴検索、 cd -up で上方検索が可能になります。
また、 cd -, cd .. がデフォルトの挙動($OLDPWD, .. への移動)に戻ります。

キーボード・ショートカットとしては履歴検索に Ctrl-u + r を割り当てました。
cd -up には割り当てていませんが、覚えていれば問題ないです。たまに便利。

結びに

bash 環境に enhancd を導入し、 cd -, cd .. の挙動を変えずに使える機能拡張をしました。

enhancd をご利用の方や、これから利用される方の参考となれば幸いです。

余談

もう1つ enhancd による cd の挙動変更でときどき戸惑うのは、 cd 単体で実行したときです。
enhancd ではこのときもディレクトリ履歴検索による選択画面になります。

これはとりあえず諦めて受け入れることにしたのですが、今回紹介した変更のように、デフォルトの挙動を変更しないオプションがあってもいいかもしれません。

追記:こちらについてもパッチを送って、デフォルトの挙動を変更しないオプションをサポートしてもらいました。

参考

See Also

脚注

Bash Infinity よりずっと前に Bash on Rails なるものを作った人がいると記録しておく

備忘録を兼ねて、ブログを書いておきます。

きっかけは、昨日のはてブでホットエントリに上がっていた下の記事です。

元記事から、GitHubソースコードにたどりついて、少しだけ内部のコードを読みました。

https://github.com/niieani/bash-oo-framework

どうも、Pure Bash で書かれているようですね。
OO という名前が現すように、オブジェクト指向で書けるように型システムをも実装しているようです。と、これは記事でも紹介されていましたが。

さて、この記事を読む数カ月前に、私は Bash on Rails なるものを作っていた人を知ったので、それを想起しました。

Bash on Rails は @emasaka さんが作った Rails モドキで、「Bash の機能だけでいかに Ruby on Rails っぽいことができるか」を実践したパロディ企画とのことです。
作者のブログ でのべ14記事にわたって紹介されています。
関係する記事の一覧は、下記の検索結果からが見やすいです。

http://emasaka.blog65.fc2.com/blog-entry-379.html?q=bash+on+rails

Bash Infinity 同様、Pure Bash かつ OO で書かれており、Rails の scaffold や db:migrate を模した機能もあります。 他に、ERB を模した eBash が実装されてたり、model や controller があったりと、まるで Ruby on Rails です。
ジョークソフトにしては力が入りすぎているような気さえします(笑)
(※もちろん、本家 Rails に有って Bash on Rails にない機能はたくさんあるでしょうけど)

ブログ記事の中で、コアの部分で面白そうな記事を1つだけ挙げるとしたら、本を読む Bash on Railsを作る(10) bashOOをbashOOで作る を推します。
オブジェクト指向をどうやって実現しているか、コードを混じえて紹介されています。
この記事だけでも一見の価値はあるかな、と。

GitHub 上では shails という名前でソースコードが公開されています。

https://github.com/emasaka/shails

Bash on Rails を初めて見つけたときは、「Bash ってここまでできるのか」と感動したものです。
そこまでやる必要があるのかはさておき。

ご興味がある方は、ソースコードや記事を読んでいただくと面白いと思います。

Bash on RailsBash Infinity の「実用性」について

Bash on Rails はそもそもジョークプロダクトで、まともな用途で使わないでね、とブログで書かれています。
こちらも内部のコードを少し追いましたが、eval など駆使して無理やりオブジェクト指向っぽく見せているような感はありました。
あくまでオブジェクト指向を「模している」だけなので、違うものだという前提を持った方がいいでしょう。

Bash Infinity は実用的なプロダクトを目指しているのかな? と思わせる雰囲気で、 GitHub 上でかなりスターもついてます。(昨日は1000弱でしたが、さっき見たら1000を超えていました。)
…が、どのくらい実用的なのかは疑問符です。テストライブラリは付属しているそうですが、Bash Infinity 自体のテストコードが見当たらないような…。。

最初に挙げた記事に書かれていたように、「各ライブラリでどのようなテクニックが使われているか」の勉強にはよさそうだな、と思いました。

余談

@emasaka さんのブログをあさっていたら、sinatra モドキも作っていたことを知りました(笑)

以下は、記事から引用:

「…"sh inatra"って言いたかっただけと違うか?」と思ったあなた、はい、そのとおりです。

Googleサイト上の個人WikiでMarkdown Hereを使うことにした

気づけば1ヶ月以上ブログの更新が滞っていたようです(汗)
いくつか小ネタが溜まっているので、隙を見て消化していきたいと思っています。

さて、半年ほど前に、日々の個人的な雑メモは こちらの Google サイト に書くことにしました。
その経緯については、下の記事でかんたんに紹介しています。

Google サイトは手軽にページ階層が編集できるなど、個人 Wiki として便利である一方で、 Markdown に対応していないというのが、そこそこ大きなペインポイントでした。

が、1ヶ月ほど前に何か手段はないかともう一度探してみたところ、 Markdown Here を見つけることができました。

で、試してみて、非常に満足したので、それからこれを使っています。

以下、Markdown Here について簡単に紹介します。

Markdown Here の使い方

  1. Markdown で記事の原稿を書く。
  2. ホットキー(デフォルトは Ctrl + Alt + M)を入力すると、HTMLにレンダリングされる。
    1. もう一度ホットキーを入力すると Markdown テキストに戻る。

自分は FirefoxChrome を常用していますが、どちらもデフォルトのホットキーのまま使っています。
また、オプションでシンタックスハイライトのスタイルで使われている highlight.js のスタイルを変更することができます。 自分は Tommorow Night Bright にしました。

Markdown Here の他のブラウザやメーラへのインストール方法については、公式サイトをご覧ください。

余談ですが、 Markdown Here は Google サイトだけでなく、 GmailBlogger など他の Google サービスでも使えます。 更には、 FacebookTumblr, Evernote でも動作するようです。
詳しくは Compatibility · adam-p/markdown-here Wiki · GitHub をご覧ください。*1

見た目はどんな風になるの?

Google サイトのこのページ で、 Markdown-Cheatsheet のソーステキストを入力して、 Markdown Here でレンダリングしてみました。

だいたい、いい感じになっていると思います。

ご興味のある方は、ぜひご覧ください。

終わりに

Google サイトを Markdown で書けるようになる Markdown Here を紹介しました。
Gmail 等でも使えますので、機会がありましたら試してみてはいかがでしょうか。

*1:こちらにはリストされていませんが、自分が試してみたところ、 Confluence Wiki でも動作しました。

#PrometheusCasual #1 に行ってきた

発表資料

@wyukawa Hadoop, Fluentd cluster monitoring with Prometheus and Grafana

@mtanda Prometheus on AWS

@tokuhirom promgen - prometheus management tool

kawamuray HBase, Kafka cluster monitoring with Prometheus and Grafana

@moznion 5分で作るprometheus exporter

Togetter まとめ

メモ

  • Prometheus の特徴
    • アーキテクチャ - https://prometheus.io/docs/introduction/overview/#architecture
    • Pull 型
      • Prometheus は監視ノードの HTTP インタフェース(Exporter)を叩いてデータを取得
      • pushgateway を使うと push 型もできる
    • 監視対象に Exporter を仕込む方法:
      • 1) 監視対象自体に HTTP エンドポイントを持たせる
      • 2) 独立した Exporter プロセスを動かす
    • データを演算できる。クエリが書ける
      • PromQL
    • Local の TimeZone 使えない
      • @kawamuray「彼らは local time に猛烈な嫌悪感を持っている」
  • Exporter を作る
    • Go, Java, Python, Ruby のクライアントライブラリがある https://github.com/prometheus?query=client
      • client_java
        • simpleclient を監視対象の Java アプリに組み込むが楽っぽい @tokuhirom
        • simpleclient_jetty, simpleclient_spring_boot 作った @tokuhirom
    • 作るのは簡単なので、どんどん作るといい @kawamuray
      • H2O の exporter (Golang) は5分で書ける @moznion
  • Prometheus の運用について
    • 楽でほぼ手間はないらしい @mtanda
      • バージョンアップ時に非互換の変更があると、設定を書き換えないといけなかったり
    • ディスクをかなり使う @mtanda
      • 1ノード 150 メトリクスで1ヶ月あたり 200MB とか
    • YAML 管理がつらい
      • Exporter の管理など
      • Solution:
        • Service Discovery
          • EC2, Consul, k8s などと連携して監視対象リストを自動更新
          • ★ @mtanda 氏の資料
        • promgen by @tokuhirom
          • WebUI ぽちぽちで監視ノード(というか Exporter ?)管理できるっぽい
            • Export/Import 機能もある?(想像)
          • バックエンドはデフォルト RDB だけど、プラガブル
            • ポータブル
            • 内部ではデプロイツールがホスト情報を管理していて、それと連携してる
          • promgen-alerting
            • Prometheus の Alertmanager から webhook でアラートを受け取り、通知する
          • 「天気がよかったので Ruby で作った」(笑)
    • 設定
      • データの保持期間がデフォルト15日
      • storage.local.memory-chunks デフォルト 1MB
        • 変える場合、max-chunks-to-persist も合わせて変更しないといけない。罠い。
  • データを長期保存する工夫 @mtanda
    • Prometheus を2台用意
    • 片方で 1時間ごとにデータをサマライズしている

所感

  • 勉強会
    • 出席率よかった。次世代監視ツールとして、注目度が高い雰囲気
    • Prometheus 手元でちょっと動かしたぐらいでほとんど情報がなかったので、ユーザ事例をたくさん聞けてよかった
  • Prometheus
    • 他の監視ツールとはアーキテクチャが異なるので、学習コストはありそう。実運用に落としこむまで試行錯誤が必要そう
    • 大規模向けと思った
    • SaaS 使いたくない、柔軟にメトリクス取りたい、技術力のあるエンジニアが揃ってる、そんなチームに向いてそう
    • promgen は公開予定はあるのだろうか?

ありがとうございました。