【Rspec】綺麗に書くを意識し始めたら読んでほしい！

# はじめに

こんにちは、エンジニア3年目の嶋田です。  
この記事を開いていただきありがとうございます！

今回の記事では、RSpecの基本的な構文と概念についてまとめました。  
最近の業務ではRSpecを使用していますが、
どういうコードが読みやすく綺麗であるのかを最近意識するようになってきました。
(気にするのが遅いですし、まだまだですが…)

その中で、理解しておくといいなと思ったことを自分自身のために備忘録としてまとめました。
よろしければ、最後までお付き合い下さい。

## 目次
- [RSpecとは](#RSpecとは)
- [describe, context, it の使い分け](#describe-context-it-の使い分け)
- [let と let! の違い](#let-と-let-の違い)
- [テストデータと変数の管理](#テストデータと変数の管理)
- [shared_examples の使い方](#sharedexamples-の使い方)
- [まとめ](#まとめ)
- [参考文献](#参考文献)

## RSpecとは
RSpecはRuby言語用のテストフレームワークです。
BDD（Behaviour Driven Development）ツール、TDD（テスト駆動開発）とも呼ばれます。
Rspecにテストコードを記述しておいてソフトウェアテストを実行すると、自動的にテストコードに沿ったバグチェックをおこなえるツールです。(満足に使いこなせたら便利すぎますよね〜)


詳しくはこちら👉 https://github.com/rspec/rspec-rails

## describe, context, it の使い分け

RSpecの基本的な構文には、`describe`、`context`、`it`があります。
`describe`、`context`、`it`の説明を読むことでテストの概要がわかることが理想的です。
例えば以下のようなコードです。
```ruby
describe "#full_name" do
  # last_nameのみ設定されている場合
  context "when only last_name is set" do
    # last_nameを返すこと
    it "returns last_name" do
      # ...
    end
  end

  # first_nameのみ設定されている場合
  context "when only first_name is set" do
    # first_nameを返すこと
    it "returns first_name" do
      # ...
    end
  end

  # last_nameとfirst_nameが設定されている場合
  context "when both last_name and first_name are set" do
    # last_nameとfirst_nameをスペース区切りで返すこと
    it "returns last_name and first_name separated by a space" do
      # ...
    end
  end
end
```

これらの役割を意識して分割し、説明を明確に書くことで読みやすいコードになると思います。

| 構文      | 役割                                                         |
|-----------|--------------------------------------------------------------|
| describe  | テスト対象をグループ化する。クラスやメソッド、機能などを説明。   |
| context   | 特定の条件や状況を示す。条件ごとにテストをグループ化。          |
| it        | 具体的なテストケースを記述する。期待値を示す。                |

### describe
`describe`は、テスト対象をグループ化するために使用します。クラスやメソッド、機能などを説明するために使います。

```ruby
RSpec.describe User, type: :model do
  describe "#full_name" do
    # "full_name"メソッドに対するテストをここに記述
  end
end
```

`describe`の中にテストの内容を記述することで、どのクラスやメソッドに対するテストなのかを明確にします。例えば、`User`モデルのテストの場合、上記のように`RSpec.describe User, type: :model do`と書くことで、このブロック内に`User`モデルに関連するテストが含まれることを示します。

### context
`context`は、特定の条件や状況を示すために使用します。
例えば、「ユーザーがログインしている場合」や「入力が無効な場合」、「ユーザー種別が管理者の場合」や「ユーザー種別が一般ユーザーの場合」などです。
特定の状態や条件をグループ化して記述します。これにより、テスト対象の状態や条件を明確に示すことができます。

```ruby
RSpec.describe User, type: :model do
  describe "#full_name" do
    context "when only last_name is set" do
      # last_nameが設定されている場合のテストをここに記述
    end

    context "when only first_name is set" do
      # first_nameが設定されている場合のテストをここに記述
    end

    context "when both last_name and first_name are set" do
      # last_nameとfirst_nameが設定されている場合のテストをここに記述
    end
  end
end

```

`context`を使うことで、条件や状態ごとにテストをグループ化できます。例えば、管理者ユーザーと通常ユーザーでテストケースが異なる場合、それぞれの`context`を用意することで、テストの意図が明確になります。

### it
`it`は、具体的なテストケースを記述するために使用します。テストの期待値を示すために使います。

```ruby
RSpec.describe User, type: :model do
  describe "#full_name" do
    context "when only last_name is set" do
      it "returns last_name" do
        user = User.new(last_name: "Smith")
        expect(user.full_name).to eq "Smith"
      end
    end

    context "when only first_name is set" do
      it "returns first_name" do
        user = User.new(first_name: "John")
        expect(user.full_name).to eq "John"
      end
    end

    context "when both last_name and first_name are set" do
      it "returns last_name and first_name separated by a space" do
        user = User.new(first_name: "John", last_name: "Smith")
        expect(user.full_name).to eq "John Smith"
      end
    end
  end
end
```

`it`ブロック内には、特定の条件下での期待する結果を記述します。例えば、`User`モデルが正しい属性を持っている場合に有効であることを確認するテストや、メールアドレスがない場合に無効であることを確認するテストを記述します。

そして！ `it`を必要以上に分けないこともポイントです。
以下のように1つの`it`ブロックに1つの`expect`を書くことは避けます。
観点が大きく異なる場合を除き、1つの`it`ブロック内で全ての検証を行います。

```ruby
RSpec.describe User, type: :model do
  describe "#activate" do
    let!(:user) { create(:user, status: :inactive) }

    it "changes status to active and sets activated_at" do
      aggregate_failures do
        user.activate
        expect(user.status).to eq :active
        expect(user.activated_at).to be_present
      end
    end
  end
end
```
これにより、テストコードが読みやすくなり、どの`expect`が失敗したかを簡単に特定できます。
`aggregate_failures`を有効にすることで、複数の`expect`の失敗をまとめて確認できます。

## let と let! の違い
`let`と`let!`はテストデータを準備するための方法ですが、評価タイミングが異なります。

`let`は遅延評価され、初めて参照されたときに実行されますが、`let!`はテストの実行前に一度だけ評価されます。

通常は`let`を使いますが、テストデータが意図したタイミングで評価されないことによる問題を防ぐために`let!`を使用することがあります。
遅延評価される`let`ではなく、常に評価される`let!`を使用し、意図せぬタイミングでのレコード作成を防ぎます。

```ruby
RSpec.describe User, type: :model do
  describe "User validations" do
    let!(:user) { create(:user) }

    it "is valid with valid attributes" do
      expect(user).to be_valid
    end

    it "is not valid without an email" do
      user.email = nil
      expect(user).not_to be_valid
    end
  end
end
```

`let!`を使用することで、テストデータが確実に生成され、テストの実行順序によって予期しない結果が発生することを防ぐことがあります。

### 上書きは避ける
そして！
`let`や`let!`の上書きは避けたほうが良いです。上書きをすると定義箇所を追跡するのが難しくなり、テストの可読性が低下します。上書きが必要になる場合は、`describe`や`context`の分割を見直すべきかもしれません。

以下は、`let`や`let!`を上書きしている例です。
これにより定義箇所を追跡するのが難しくなり、テストの可読性が低下します。

```ruby
# 悪い例：上書きしている
RSpec.describe User, type: :model do
  let!(:user) { create(:user, role: :admin) }

  describe "#role_behavior" do
    context "when the user is an admin" do
      it "performs admin specific behavior" do
        expect(user.role).to eq :admin
        # 管理者特有の振る舞いに関するテスト内容
      end
    end

    context "when the user is a guest" do
      let!(:user) { create(:user, role: :guest) }  # 上書きしている

      it "performs guest specific behavior" do
        expect(user.role).to eq :guest
        # ゲスト特有の振る舞いに関するテスト内容
      end
    end
  end
end
```

```ruby
# 良い例：上書きしない
RSpec.describe User, type: :model do
  describe "#role_behavior" do
    context "when the user is an admin" do
      let!(:user) { create(:user, role: :admin) }

      it "performs admin specific behavior" do
        expect(user.role).to eq :admin
        # 管理者特有の振る舞いに関するテスト内容
      end
    end

    context "when the user is a guest" do
      let!(:user) { create(:user, role: :guest) }

      it "performs guest specific behavior" do
        expect(user.role).to eq :guest
        # ゲスト特有の振る舞いに関するテスト内容
      end
    end
  end
end
```
上記の例では、`user`を上書きせずに、それぞれの`context`ブロックで独立した`user`オブジェクトを作成しています。
これにより、上書きによる混乱を避けることができます。

### 必要なセットアップは before で行う

セットアップも含め、テストで参照しないがレコードとして存在していて欲しいものを作成する場合は、`let!`ではなく`before`を使用します。

```ruby
RSpec.describe User, type: :model do
  before do
    create(:irrelevant_record) # テストで参照しないが、存在してほしいレコード
  end

  it "is valid with valid attributes" do
    user = FactoryBot.build(:user)
    expect(user).to be_valid
  end
end
```
このように、必要なセットアップは`before`ブロックを使って行い、テストに直接関係するものだけを`let!`で定義します。


`let`を使うべきケースは、遅延評価でなければ実現できない場合のみです。
しかし、実際にはそのようなケースはほとんどありません。
原則として`let!`を使用したほうがテストデータが明確になります。

## テストデータと変数の管理

テストデータや変数を適切に管理することがコードの可読性を向上させます。

### 必要な箇所でローカル変数を使用する

`let`や`let!`である必要がないものは、ローカル変数を基本的に使用します。
ローカル変数はスコープが狭いため、読みやすく、意図が明確になります。

```ruby
RSpec.describe User, type: :model do
  let!(:user) do
    company = create(:company)
    create(:user, company: company)
  end

  it "is associated with the correct company" do
    expect(user.company).to be_present
  end
end
```

### describe 内にテストデータを配置する

`describe`の外にテストデータを置くと、スコープが広がり、意図せずにデータが共有される可能性があります。全テストケースで共通して使用するデータ以外は、テストデータは`describe`や`context`内に定義すると良いと思います。
ただし、全テストケースで共通して使用する設定やデータがある場合は、`before(:all)`などを使うことで適切に管理することも可能です。

前提条件の違うテストケースが追加された場合に意図せず参照しない`let`や`let!`の上書きが起きてしまうことになりがちがだからです。

```ruby
RSpec.describe User, type: :model do
  let!(:user) { create(:user) }  # describeの外に定義している

  describe "#valid?" do
    it "returns true with valid attributes" do
      expect(user).to be_valid
    end
  end

  describe "#email" do
    let!(:user) { create(:user, email: "test@example.com") }

    it "returns the correct email" do
      expect(user.email).to eq "test@example.com"
    end
  end
end
```

### テストデータを update しない

作成したデータを`update`で変更せず、FactoryBotのtraitsやattributesを使って必要な状態を直接設定しましょう。

FactoryBotについては前回記事を書きました。
お時間ある時にぜひ💁‍♂️

https://qiita.com/shimada_slj/items/ce21b34a68eed652f273

これにより、テストデータの最終状態が明確になります。

```ruby
# 悪い例：テストデータをupdateしている
RSpec.describe User, type: :model do
  let!(:user) { create(:user) }

  it "updates the email" do
    user.update(email: "new_email@example.com")  # テストデータをupdateしている
    expect(user.email).to eq "new_email@example.com"
  end
end
```

```ruby
# 良い例：FactoryBotのtraitsやattributesを使用
RSpec.describe User, type: :model do
  let!(:user) { create(:user, email: "new_email@example.com") }

  it "has the correct email" do
    expect(user.email).to eq "new_email@example.com"
  end
end
```

### アプリケーションロジックを使わずにデータを作成する

テストデータの作成にはアプリケーションロジックを使用せず、FactoryBotや手動での設定を使います。
これにより、ロジックのバグや変更に影響されないテストデータを保持できます。

```ruby
# 悪い例：アプリケーションロジックを使ってテストデータを作成
RSpec.describe User, type: :model do
  let!(:user) { User.create_with_business_logic }

  it "is valid" do
    expect(user).to be_valid
  end
end

# 良い例：FactoryBotを使ってテストデータを作成
RSpec.describe User, type: :model do
  let!(:user) { create(:user) }

  it "is valid" do
    expect(user).to be_valid
  end
end
```

## shared_examples の使い方

`shared_examples`は、複数のテストケースで共通するテストコードを再利用するための機能です。これを効果的に使うことで、テストコードの重複を減らし、**DRY (Don't Repeat Yourself) 原則**を守ることができます。

### shared_examples の定義と使用

まず、`shared_examples`を定義します。これにより、共通のテストロジックをまとめることができます。

```ruby
# 共通のテストケースを定義
RSpec.shared_examples "a user with valid attributes" do
  it "is valid" do
    expect(subject).to be_valid
  end

  it "has an email" do
    expect(subject.email).to be_present
  end
end
```

次に、この`shared_examples`を使用するテストで`it_behaves_like`を使って呼び出します。

```ruby
RSpec.describe User, type: :model do
  describe "Admin user" do
    subject { build(:user, :admin) }

    it_behaves_like "a user with valid attributes"
  end

  describe "Regular user" do
    subject { build(:user) }

    it_behaves_like "a user with valid attributes"
  end
end
```

このように、共通のテストコードを`shared_examples`にまとめて再利用することで、テストの重複を減らし、コードの可読性とメンテナンス性を向上させることができます。

### shared_examples を慎重に使用する

`shared_examples`は便利ですが、慎重に使用する必要があります。スコープが広がりすぎると、テストの意図が曖昧になり、理解が難しくなることがあります。また、ファイルを跨いで共有すると、テストの追跡が難しくなることがあります。

以下のポイントを考慮して`shared_examples`を使用しましょう!

1. **適切なスコープで使用する**
    - 必要以上に広い範囲で使わず、関連するテストケースだけで使用します。
2. **ファイルを跨いで共有しない**
    - 同じファイル内で使用し、関連するテストが一緒に管理されるようにします。
3. **適切な命名を行う**
    - `shared_examples`の名前がその内容を明確に表すようにします。

### モジュールを利用した共有

場合によっては、`shared_examples`よりもモジュールを利用して共通ロジックを共有する方が適していることもあります。特に、メソッドを共有する場合に有効です。

```ruby
module UserHelpers
  def create_valid_user
    create(:user, email: "valid@example.com")
  end
end

RSpec.configure do |config|
  config.include UserHelpers
end

RSpec.describe User, type: :model do
  describe "Admin user" do
    it "is valid" do
      user = create_valid_user
      expect(user).to be_valid
    end
  end
end
```

このように、`shared_examples`を適切に使用し、場合によってはモジュールを活用することで、テストコードの品質を高めることができます。

## まとめ

### RSpecとは
- **RSpec**はRuby言語用のテストフレームワークです。BDD（Behaviour Driven Development）とTDD（テスト駆動開発）の手法をサポートします。
- 単体でも使えますが、Ruby on Railsなどと組み合わせることでその真価を発揮します。

### describe, context, it の使い分け
- **describe**
    - テスト対象をグループ化します。クラスやメソッド、機能などを説明します。
- **context**
    - 特定の条件や状況を示します。条件ごとにテストをグループ化します。
- **it**
    - 具体的なテストケースを記述します。期待値を示します。`aggregate_failures`を使うことで、複数の`expect`をまとめて確認できます。

### let と let! の違い
- **let**
    - 遅延評価され、初めて参照されたときに実行されます。
- **let!**
    - テストの実行前に一度だけ評価されます。テストデータが意図したタイミングで評価されないことによる問題を防ぐために使用します。

### テストデータと変数の管理
- **ローカル変数**
    - 必要な箇所でローカル変数を使用し、スコープを狭くすることで読みやすさを向上させます。
- **describe内にデータを配置**
    - テストデータは`describe`や`context`内に定義し、スコープを広げすぎないようにします。
- **updateを避ける**
    - テストデータの作成にはFactoryBotのtraitsやattributesを使い、updateを避けます。
- **アプリケーションロジックを使わない**
    - テストデータの作成にはアプリケーションロジックを使用せず、FactoryBotや手動での設定を使います。

### shared_examples の使い方
- **shared_examples**
    - 複数のテストケースで共通するテストコードを再利用するための機能です。
- **使用の注意点**
    - 適切なスコープで使用し、ファイルを跨いで共有しない、適切な命名を行うことが重要です。

ここまで読んでいただきありがとうございます。
`let!`激推しの記事も読みましたし、**読みやすいコード**が人それぞれ違うようで難しいですね…
この記事での紹介はあくまでも1つの方法として捉えていただければありがたいです。
そして、私自身まだまだ勉強中なので、もしもっと良い方法があればぜひ教えていただきたいです。



## 参考文献
https://github.com/willnet/rspec-style-guide

https://speakerdeck.com/willnet/clean-test-code-revised