Hatena::ブログ(Diary)

shouhの日記

2016-12-28

RSpec で Example Group や Example 内スコープの実行順序がよくわからんので調べてみた

ちょっと直感的じゃなかった(まだ RubyRSpec の文化?に慣れてないだけだろうが)ので、調べてみた。


結論

  1. Example Group を上から順に読み込み、木構造をつくる
    • その際 Example Group 直下のブロック内の文は評価される
  2. 解析した木構造を元に、順に Example を実行する

いう感じ。

調査に使ったやつ order.rb

describe 'D1' do
  puts 'in D1'

  it '' do
    puts 'it in D1'
  end

  describe 'D1-D1' do
    puts 'in D1-D1'

    it '' do
      puts 'it in D1-D1'
    end

  end

  describe 'D1-D2' do
    puts 'in D1-D2'

    describe 'D1-D2-D1' do
      puts 'in D1-D2-D1'

      it '' do
        puts 'it in D1-D2-D1'
      end
    end
  end
end

describe 'D2' do
  describe 'D2-D1' do
    puts 'in D2-D1'

    it '' do
      puts 'it in D2-D1'
    end
  end

  puts 'in D2'

  it '' do
    puts 'it in D2(1)'
  end

  it '' do
    puts 'it in D2(2)'
  end

  describe 'D2-D2' do
    puts 'in D2-D2'
    it '' do
      puts 'it in D2-D2'
    end
  end
end

調査結果 標準出力

Rakefile などは載せてないので適当に補ってください。

$ rake order
/usr/bin/ruby2.1 -I/var/lib/gems/2.1.0/gems/rspec-support-3.2.2/lib:/var/lib/gems/2.1.0/gems/rspec-core-3.2.3/lib /var/lib/gems/2.1.0/gems/rspec-core-3.2.3/exe/rspec --pattern 127.0.0.1,spec/order_spec.rb
in D1
in D1-D1
in D1-D2
in D1-D2-D1
in D2-D1
in D2
in D2-D2

D1
it in D1
  example at ./spec/order_spec.rb:5
  D1-D1
it in D1-D1
    example at ./spec/order_spec.rb:12
  D1-D2
    D1-D2-D1
it in D1-D2-D1
      example at ./spec/order_spec.rb:24

D2
it in D2(1)
  example at ./spec/order_spec.rb:42
it in D2(2)
  example at ./spec/order_spec.rb:46
  D2-D1
it in D2-D1
    example at ./spec/order_spec.rb:35
  D2-D2
it in D2-D2
    example at ./spec/order_spec.rb:52

Finished in 0.00294 seconds (files took 0.17938 seconds to load)
7 examples, 0 failures

2016-12-15

RSpec で Example 間で状態変数を共有したい

たとえば変数変数 state に対して、一つ目の it 内で state に結果を入れ、二つ目の it 内でその中身を検証する、みたいなケース。

RSpec は使い方も仕組みもテスト実行順序もカオスで正直意味不明なので、かなり苦戦した。

結論

Example(it や example)を並べてるスコープ(ブロック)の中で、使いたい変数を空定義しておく。

サンプル

# Responce と API は便宜上のテキトーなもの.
class Responce
  attr_accessor :status_code, :body
  def initialize()
    @status_code = nil
    @body = nil
  end

  def valid?()
    if @body.nil? then
      return false
    end
    return true
  end
end

class API
  def self.get()
    responce = Responce.new()
    responce.status_code = 200
    responce.body = "Information from API!"
    responce
  end
end

describe "上位スコープで空定義" do

  # ★ ここで空定義しておく.
  responce = nil

  it "項番1. APIから情報を取得できること" do
    responce = API.get()
    expect(responce.status_code).to equal(200)
  end

  it "項番2. APIから取得した情報が正しいこと" do
    # ★ 変数 responce を空定義しているおかげで, 
    #    項番1で取得した情報を利用できる.
    #
    #    空定義がない場合, 未定義ローカル変数とみなされ
    #    undefined local variable or method `responce' エラー.
    expect(responce).not_to be nil
    ret = responce.valid?
    expect(ret).to be true
  end
end

なんでこの結論?

代替案に関する疑問。

  • Q: before じゃダメなの?
    • A: ダメ。before はテストケース(it)毎に毎回実行されるので、項番1テストで取得した結果を項番2テストから使えない
  • Q: let じゃダメなの?
    • A: ダメ。let には遅延評価キャッシュの仕組みがあるが、それは同一テストケースの話であって、上記みたいに項番1と2でテストケース分けてる時は before と同じく毎回 let 内ブロックが評価されてしまい、やはり項番1テストで取った結果を項番2テスト時に見れない
  • Q: shared_context はダメなの?
    • A: ダメ。shared_context 内で空定義したものは includee 側の example group 内では認識されない。認識させるなら before や let を使う必要があるが、上記と同じ問題にぶち当たる
  • Q: じゃあ項番1と2を同じテストケースにぶちこんだら?
    • A: テストコード及びテスト実行結果出力の可読性が損なわれるからヤダ。まさか「puts "項番1..."」を書く、とか言わないよね。

仕組みに関する疑問

  • Q: 空定義は必要なの?
    • A: 必要。そうしないと項番2テストで「undefined local variable or method `responce'」エラーが出る。
  • Q: なんでこういう動作になるかがわからん。RSpec どうなってんの?
    • A: 想像だけど、RSpec はたぶんこんな動きになってる。
      • 1.example group(describeなど)を全て辿ってテスト実行順序及び階層関係をつくる。この時、describe 直下に書かれているコードは全て実行される
      • 2. 辿り終えたら、作った実行順序及び階層関係のデータに従い、example(itなど)を順番に実行していく

所感

  • RSpec の慣習にこだわらず、さっさと外側スコープに空定義するだけという簡易的なやり方にしておくべきだった。まあ RSpec についてちょっとは勉強になったのでよしとしよう。
  • そもそも外側スコープで変数定義するという書き方が想定されてない or おかしいんだろうけど。