RSpecの書き方で最近悩んだこと3つとその解決法について


RSpecの書き方について、最近躓いた３選です

検証環境

- ruby: 2.6.5
- rails: 6.0.3.2
- rspec-rails: 3.9.0


## module単体に対してテストを書きたい

moduleをいろんなクラスで使用することを想定するとき、テストを特定のクラスに依存させて書くのはよくなさそうです。

そんなとき、どうすればよいか悩んだのですが、こんなふうに書くと、特定のクラスに依存せず、ｍoduleをinclude/extendしたクラスのテストができそうでした。

### `Class.new { include Module }` して作成したダミーのクラスに対しテストする

```ruby
# app/models/concerns/good_module.rb

module GoodModule
  def ok
    'ok!'
  end
end
```


```ruby
# spec/models/concerns/good_module_spec.rb

describe GoodModule do
  context '特異メソッドとして使う場合' do
    let(:dummy_class) { Class.new { extend GoodModule } }
    subject { dummy_class.ok }

    it { expect(subject).to eq 'ok!' }
  end

  context 'インスタンスメソッドとして使う場合' do
    let(:dummy_class) { Class.new { include GoodModule } }
    let(:dummy_class_instance) { dummy_class.new }
    subject { dummy_class_instance.ok }

    it { expect(subject).to eq 'ok!' }
  end
end
```

こんな感じで書いてみます。

```bash
$ bundle exec rspec spec/models/concerns/good_module_spec.rb                         
..

Finished in 0.0046 seconds (files took 1.79 seconds to load)
2 examples, 0 failures
```

実行できました！


## インスタンス変数の書き込みメソッドをモックしたい

モックを利用すると、テストしやすい場面があると思います。

```ruby
# app/models/concerns/sendable.rb

module Sendable
  def send_mail(message, email)
    message.to = email
    puts 'send!'
  end
end
```

こんな感じの処理を作りましたが、messageクラスをまだ作っていません。
モックにして先にテストだけしてみたいと思います。

```ruby
# spec/models/concerns/sendable_spec.rb

describe Sendable do
  let(:dummy_class) { Class.new { extend Sendable } }
  let(:message) { double('message') }

  subject { dummy_class.send_mail(message, 'example@example.com') }

  before do
    allow(message).to receive(:to)
  end

  it { subject }
end
```

いざ実行すると…

```bash
$ bundle exec rspec spec/models/concerns/sendable_spec.rb 
F

Failures:

  1) Sendable 
     Failure/Error: message.to = email
       #<Double "message"> received unexpected message :to= with ("example@example.com")
     # ./app/models/concerns/sendable.rb:6:in `send_mail'
     # ./spec/models/concerns/sendable_spec.rb:9:in `block (2 levels) in <top (required)>'
     # ./spec/models/concerns/sendable_spec.rb:15:in `block (2 levels) in <top (required)>'

Finished in 0.01396 seconds (files took 1.74 seconds to load)
1 example, 1 failure

```

`received unexpected message` となってしまいました。
これだと想定通りに動きません。

### attr_accessorの対象となるインスタンス変数に=をつける

ただ、よく考えてみると、attr_accessorの定義は

https://docs.ruby-lang.org/ja/latest/method/Module/i/attr_accessor.html

```ruby
def name
  @name
end
def name=(val)
  @name = val
end
```

こうなので、モックも

```ruby
# spec/models/concerns/sendable_spec.rb

  before do
    # =を追加する
    allow(message).to receive(:to=)
  end

```

これが正しかったのでした。


## メソッドチェーンをモックする

例えば、`Time.zone.now.hour`のようなメソッドチェーンが返す値をモックしたいときがあると思います。

```ruby
# app/models/concerns/checkable.rb

module Checkable
  def ok
    Time.zone.now.hour == 10
  end
end
```

早速RSpecを書いてみます。

```ruby
# spec/models/concerns/checkable_spec.rb

describe Checkable do
  let(:dummy_class) { Class.new { extend Checkable } }

  subject { dummy_class.ok }

  before do
    allow(Time).to receive('zone.now.hour').and_return(10)
  end

  it { expect(subject).to be true }
end
```


```bash
$ bundle exec rspec spec/models/concerns/checkable_spec.rb
F

Failures:

  1) Checkable 
     Failure/Error: allow(Time).to receive('zone.now.hour').and_return(10)
       Time does not implement: zone.now.hour
     # ./spec/models/concerns/checkable_spec.rb:11:in `block (2 levels) in <top (required)>'

```

勘で書いてみましたが、やっぱり無理でした。。

### `receive_message_chain`を使う

そういう場合は、RSpec側にこれを想定したメソッドが用意されているのでそれを利用します。

```ruby
# spec/models/concerns/checkable_spec.rb

  before do
    allow(Time).to receive_message_chain(:zone, :now, :hour).and_return(10)
  end
```

```
$ bundle exec rspec spec/models/concerns/checkable_spec.rb
.

Finished in 0.00916 seconds (files took 1.65 seconds to load)
1 example, 0 failures
```

これで、無事、メソッドチェーンでも、特定の値を返すことができました。

## 終わり

RSpecのドキュメントを読むと、こんな書き方できたんだという発見が多いです。
テストに助けられることが多いので、今後も転ばぬ先の杖として、需要にあったテストを書けるように、RSpec力を鍛えていきたいと思いました。