RSpec再履修(RSpec Core編)

# まえおき
改めてRSpecについてRelishを流し読みして学びのあったところをチェックしてみました。
使いやすそうだなと思った一部のみ抜粋しているため、網羅しているわけではないです。
また、量が多いのでとりあえずRspec Coreだけ見ました。
https://relishapp.com/rspec/rspec-core/v/3-5/docs

```
$ rspec -v
3.5.4
```

# shared_examples

テスト項目を共有して再利用できます。

```rb
shared_examples "共有したいテスト項目" do
  it { expect(1).to eq(1) }
end

describe "テスト" do
  it_behaves_like "共有したいテスト項目"
end
```

```vim
$ bundle exec rspec spec/sample/shared_examples_spec.rb --format doc

テスト
  behaves like 共有したいテスト項目
    should eq 1
```

shared_examplesは引数をとることができます。

```rb

class Food; end
class Sushi < Food; end
class Drink; end
class Tea < Drink; end

shared_examples "親クラス確認" do |child, parent|
  it "#{child}の親クラスは#{parent}である" do
    expect(child.superclass).to eq(parent)
  end
end

describe "それぞれの親クラスを確認" do
  it_behaves_like "親クラス確認", Sushi, Food
  it_behaves_like "親クラス確認", Tea,   Drink
end
```

```vim
$ bundle exec rspec spec/sample/shared_examples_spec.rb --format doc

それぞれの親クラスを確認
  Sushiの親クラスはFoodである
  Teaの親クラスはDrinkである

Finished in 0.00301 seconds (files took 0.2289 seconds to load)
2 examples, 0 failures
```

https://relishapp.com/rspec/rspec-core/v/3-5/docs/example-groups/shared-examples


# shared_context


条件を共有することができます。

```rb
shared_context "共有する条件" do
  let(:shared_let) { "hello" }
  before do
    shared_let.concat("world")
  end
end

describe "shared_contextのテスト" do
  include_context "共有する条件"
  it { expect(shared_let).to eq("helloworld") }
end
```

```vim
$ bundle exec rspec spec/sample/shared_context_spec.rb --format doc

shared_contextのテスト
  should eq "helloworld"
```

configに宣言を定義しておくと、`include_context` を使わずに共有できます。
`build_sushi` という定義をしてみます。

```rb:spec/spec_helper_for_shared_context.rb
class Sushi
  attr_accessor :name
  def initialize(name=nil)
    @name = name
  end
end

shared_context "寿司を握る" do
  let(:sushi) { Sushi.new("トロ") }
end

RSpec.configure do |rspec|
  rspec.include_context "寿司を握る", build_sushi: true
end
```

```rb:spec/sample/shared_context.rb
require "spec_helper_for_shared_context.rb"

describe "宣言で共有する", build_sushi: true do
  it { expect(sushi.name).to eq("トロ") }
end

describe "個別に共有する" do
  let(:sushi) { Sushi.new("あなご") }
  it { expect(sushi.name).to eq("あなご") }

  it "トロ定義済み", build_sushi: true do
    expect(sushi.name).to eq("トロ")
  end

end
```

https://relishapp.com/rspec/rspec-core/v/3-5/docs/example-groups/shared-context


# Command line


RSpec実行時にオプションを指定することで特定のテストを実行、除外などが可能です。

- [--example](https://relishapp.com/rspec/rspec-core/v/3-5/docs/command-line/example-option)
- [--tag](https://relishapp.com/rspec/rspec-core/v/3-5/docs/command-line/tag-option)
- [行数指定](https://relishapp.com/rspec/rspec-core/v/3-5/docs/command-line/line-number-appended-to-file-path)
- [Only Failures](https://relishapp.com/rspec/rspec-core/v/3-5/docs/command-line/only-failures)




## --example, --tag

```rb:spec/sample/command_line/command_line1_spec.rb
describe "Command Line テスト ひとつめ" do
  it "ログインに関するテスト その1" do; end
  it "ログインに関するテスト その2", required: false do; end
  it "ログインに関するテスト その3", required: true, type: 'important' do; end
  it "ログアウトに関するテスト" do; end
end
```

```rb:spec/sample/command_line/command_line2_spec.rb
describe "Command Line テスト ふたつめ" do
  it "レコード作成に関するテスト その1", type: 'heavy' do; end
  it "レコード作成に関するテスト その2" do; end
  it "レコード作成に関するテスト その3", required: true do; end
  it "レコード削除に関するテスト", type: 'heavy' do; end
end

describe "Command Line テスト みっつめ" do
  it "関係のない名前" do; end
end
```


`--example 'テスト'` を指定すると説明に`テスト`を含むものを実行する。
itの命名に使われていなくても、describeに含めばそれも実行される。

```vim
$ bundle exec rspec spec/sample/command_line --example 'テスト' --format doc
Run options: include {:full_description=>/テスト/}

Command Line テスト ひとつめ
  ログインに関するテスト その1
  ログインに関するテスト その2
  ログインに関するテスト その3
  ログアウトに関するテスト

Command Line テスト ふたつめ
  レコード作成に関するテスト その1
  レコード作成に関するテスト その2
  レコード作成に関するテスト その3
  レコード削除に関するテスト

Command Line テスト みっつめ
  関係のない名前

Finished in 0.0036 seconds (files took 0.35518 seconds to load)
9 examples, 0 failures
```


`--example 'その2'` を指定すると説明に`その2`を含むものしか実行しない。

```vim
$ bundle exec rspec spec/sample/command_line --example 'その2' --format doc
Run options: include {:full_description=>/その2/}

Command Line テスト ひとつめ
  ログインに関するテスト その2

Command Line テスト ふたつめ
  レコード作成に関するテスト その2

Finished in 0.00192 seconds (files took 0.19095 seconds to load)
2 examples, 0 failures
```



`--tag required` を指定すると `required: true` がついているものしか実行しない。

```vim

$ bundle exec rspec spec/sample/command_line --tag required --format doc
Run options: include {:required=>true}

Command Line テスト ひとつめ
  ログインに関するテスト その3

Command Line テスト ふたつめ
  レコード作成に関するテスト その3

Finished in 0.00132 seconds (files took 0.28193 seconds to load)
2 examples, 0 failures
```



`--tag ~type:heavy` を指定すると、`type:heavy`**ではない**ものを実行する。

```vim
$ bundle exec rspec spec/sample/command_line --tag ~type:heavy --format doc
Run options: exclude {:type=>"heavy"}

Command Line テスト ひとつめ
  ログインに関するテスト その1
  ログインに関するテスト その2
  ログインに関するテスト その3
  ログアウトに関するテスト

Command Line テスト ふたつめ
  レコード作成に関するテスト その2
  レコード作成に関するテスト その3

Command Line テスト みっつめ
  関係のない名前

Finished in 0.00321 seconds (files took 0.20338 seconds to load)
7 examples, 0 failures

```



## 行数指定

```rb:spec/sample/command_line/line_number_spec.rb
describe "行数指定の確認" do
  it "2行目" do; end

  it "4行目" do
    expect(1).to eq(1)
  end

  it "8行目" do; end
end
```


ファイル名のあとに`:行番号`で特定のテストを実行できます。

```vim

$ bundle exec rspec spec/sample/command_line/line_number_spec.rb:2 --format doc
Run options: include {:locations=>{"./spec/sample/command_line/line_number_spec.rb"=>[2]}}

行数指定の確認
  2行目

Finished in 0.00093 seconds (files took 0.30578 seconds to load)
1 example, 0 failures
```



## --only-failures

設定をすると失敗したテストのみ再実行するようにできます。

```rb:spec/spec_helper_for_failure.rb
RSpec.configure do |config|
  config.example_status_persistence_file_path = "spec/examples.txt"
end
```

```rb:spec/sample/command_line/failure_spec.rb
require "spec_helper_for_failure"

describe "only failures 確認" do
  it "成功1" do
    expect("文字列".class).to eq(String)
  end

  it "失敗1" do
    expect(1.class).to eq(String)
  end

  it "成功2" do
    expect(1.class).to eq(Fixnum)
  end

  it "失敗2" do
    expect([1].class).to eq(Fixnum)
  end
end
```


これで `$ bundle exec rspec spec/sample/command_line/failure_spec.rb` を実行すると、2つテストが失敗し、txtファイルが生成される。

```text:spec/examples.txt
example_id                                      | status | run_time        |
----------------------------------------------- | ------ | --------------- |
./spec/sample/command_line/failure_spec.rb[1:1] | passed | 0.00211 seconds |
./spec/sample/command_line/failure_spec.rb[1:2] | failed | 0.09153 seconds |
./spec/sample/command_line/failure_spec.rb[1:3] | passed | 0.00024 seconds |
./spec/sample/command_line/failure_spec.rb[1:4] | failed | 0.00239 seconds |
```


その後、 `--only-failure` オプションをつけて実行すると、失敗したテストのみ再実行できる。

```vim
$ bundle exec rspec spec/sample/command_line/failure_spec.rb --only-failure                               2.3.1
Run options: include {:last_run_status=>"failed"}
FF

Failures:

  1) only failures 確認 失敗1
     Failure/Error: expect(1.class).to eq(String)

       expected: String
            got: Fixnum

       (compared using ==)

       Diff:
       @@ -1,2 +1,2 @@
       -String
       +Fixnum

     # ./spec/sample/command_line/failure_spec.rb:9:in `block (2 levels) in <top (required)>'

  2) only failures 確認 失敗2
     Failure/Error: expect([1].class).to eq(Fixnum)

       expected: Fixnum
            got: Array

       (compared using ==)

       Diff:
       @@ -1,2 +1,2 @@
       -Fixnum
       +Array

     # ./spec/sample/command_line/failure_spec.rb:17:in `block (2 levels) in <top (required)>'

Finished in 0.10357 seconds (files took 0.64171 seconds to load)
2 examples, 2 failures

Failed examples:

rspec ./spec/sample/command_line/failure_spec.rb:8 # only failures 確認 失敗1
rspec ./spec/sample/command_line/failure_spec.rb:16 # only failures 確認 失敗2

```


また、 `--next-failure` オプションを活用することで少しずつ修正を進めていける様子。（まだ実際にこれはあまり活用したことがない...。)

詳細はRelishを参照してください！
https://relishapp.com/rspec/rspec-core/v/3-5/docs/command-line/only-failures


# around hooks

`around`でテスト実行前後に処理することができます。
https://relishapp.com/rspec/rspec-core/v/3-5/docs/hooks/around-hooks

```rb:spec/sample/around_hooks.rb
describe "around hook" do
  around(:example) do |example|
    puts "実行前"
    example.run
    puts "実行後"
  end
  it { puts "テスト実行" }
end
```

```vim
$ bundle exec rspec spec/sample/around_hooks.rb 
実行前
テスト実行
実行後
.

Finished in 0.00125 seconds (files took 0.18901 seconds to load)
1 example, 0 failures
```


# Metadata


## Current example

`it`の説明の内容を検証に含むことができる。
`shared_example`とかと組み合わせたらもっと有用な感じに書けそう？
https://relishapp.com/rspec/rspec-core/v/3-5/docs/metadata/current-example


```rb:spec/sample/metadata_description_spec.rb
class Sushi
  attr_accessor :name
  def initialize(name)
    @name = name
  end
end

describe "description取得の確認" do
  let(:sushi) {|example| Sushi.new(example.description) }
  it "甘エビ" do
    expect(sushi.name).to eq("甘エビ")
  end
  it "いくら" do
    expect(sushi.name).to eq("いくら")
  end
end
```


## described_class
`describe`にセットされている値を扱うことができる。
これが存在を知ってから一番活用しているやつです。便利。
https://relishapp.com/rspec/rspec-core/v/3-5/docs/metadata/described-class


```rb:spec/sample/metadata_described_class_spec.rb
class Food; end
class Sushi < Food; end
class Sweet < Food; end

shared_examples "Foodを継承している" do
  it { expect(described_class.superclass).to eq(Food) }
end

describe Sushi do
  it_behaves_like "Foodを継承している"
end

describe Sweet do
  it_behaves_like "Foodを継承している"
end
```


# Conditional Filters


各項目に`if: false`, もしくは`unless: true`をつけると実行時に無視されます。
https://relishapp.com/rspec/rspec-core/v/3-5/docs/filtering/conditional-filters

```rb:spec/sample/conditional_filters_spec.rb
describe "if: true の describe", if: true do
  it("if: true なので含まれる", if: true) {}
  it("if: false なので除外", if: false) {}
  it("unless: true なので除外", unless: true) {}
  it("unless: false なので含まれる", unless: false) {}
end

describe "if: false の describe", if: false do
  it("if: true なので含まれる", if: true) {}
  it("if: false なので除外", if: false) {}
  it("unless: true なので除外", unless: true) {}
  it("unless: false なので含まれる", unless: false) {}
end
```


```vim
$ bundle exec rspec spec/sample/conditional_filters_spec.rb --format doc

if: true の describe
  if: true なので含まれる
  unless: false なので含まれる

if: false の describe
  if: true なので含まれる

Finished in 0.00162 seconds (files took 0.23367 seconds to load)
3 examples, 0 failures
```


ちなみに `--tag` でわざと指定しても filterd out と言われてしまいました。

```vim
$ bundle exec rspec spec/sample/conditional_filters_spec.rb --tag if:false --format doc
Run options: include {:if=>false}

All examples were filtered out

Finished in 0.00079 seconds (files took 0.47536 seconds to load)
0 examples, 0 failures
```


# Pending and Skipped



`pending`(未決定、保留中)と`skip`(とばす)を設定しておくことができます。
`pending`は現時点で失敗してしまうテスト項目に付与するものなので、成功するテストがpendingしていると失敗扱いとなります。
一方`skip`は単純にskipしたいテスト項目に対して付与します。
`pending`、`skip`ともに理由をつけておくと実行時に表示してくれます。


https://relishapp.com/rspec/rspec-core/v/3-5/docs/pending-and-skipped-examples

英語力が足りなかったので下記の記事を参考にしました！
[Rspec の pending と skip って何が違うの？ - Qiita](http://qiita.com/tyamagu2/items/fe75ebfcfd643d3d760d)


```rb:spec/sample/pending_and_skipped_spec.rb
# pending specs
describe "Pendingなテスト" do
  it "pendingです" do
    pending "まだです"
    fail
  end
  it "エラーが無いけどpending" do
    pending "エラーが無いときはpendingはFailed"
    expect(1).to eq(1) # エラーが無いpendingは不適とみなされてFAILED
  end
end

# skip specs
describe "テストをskipする" do
  it("skipします その1") { skip("skipする理由") }
  it("skipします その2", skip: "skipする理由") {}
  xit("skipします その3") { } # 一時的なskipという意味があるので理由不要
end
```


```vim
$ bundle exec rspec spec/sample/pending_and_skipped_spec.rb --format doc

Pendingなテスト
  pendingです (PENDING: まだです)
  エラーが無いけどpending (FAILED - 1)

テストをskipする
  skipします その1 (PENDING: skipする理由)
  skipします その2 (PENDING: skipする理由)
  skipします その3 (PENDING: Temporarily skipped with xit)

Pending: (Failures listed here are expected and do not affect your suite's status)

  1) Pendingなテスト pendingです
     # まだです
     Failure/Error: fail
     RuntimeError:
     # ./spec/sample/pending_and_skipped_spec.rb:5:in `block (2 levels) in <top (required)>'

  2) テストをskipする skipします その1
     # skipする理由
     # ./spec/sample/pending_and_skipped_spec.rb:15

  3) テストをskipする skipします その2
     # skipする理由
     # ./spec/sample/pending_and_skipped_spec.rb:16

  4) テストをskipする skipします その3
     # Temporarily skipped with xit
     # ./spec/sample/pending_and_skipped_spec.rb:17


Failures:

  1) Pendingなテスト エラーが無いけどpending FIXED
     Expected pending 'エラーが無いときはpendingはFailed' to fail. No Error was raised.
     # ./spec/sample/pending_and_skipped_spec.rb:7

Finished in 0.00531 seconds (files took 0.27895 seconds to load)
5 examples, 1 failure, 4 pending

Failed examples:

rspec ./spec/sample/pending_and_skipped_spec.rb:7 # Pendingなテスト エラーが無いけどpending
```




# テストリポジトリ
ごにょごにょしたファイル群を置いておきます。
https://github.com/betachelsea/sample_rspec