5
4

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

Last updated at Posted at 2024-08-09

はじめに

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

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

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

目次

RSpecとは

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

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

describe, context, it の使い分け

RSpecの基本的な構文には、describecontextitがあります。
describecontextitの説明を読むことでテストの概要がわかることが理想的です。
例えば以下のようなコードです。

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は、テスト対象をグループ化するために使用します。クラスやメソッド、機能などを説明するために使います。

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

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は、具体的なテストケースを記述するために使用します。テストの期待値を示すために使います。

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ブロック内で全ての検証を行います。

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! の違い

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

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

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

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

上書きは避ける

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

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

# 悪い例:上書きしている
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
# 良い例:上書きしない
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を使用します。

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!を使用したほうがテストデータが明確になります。

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

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

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

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

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

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

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については前回記事を書きました。
お時間ある時にぜひ💁‍♂️

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

# 悪い例:テストデータを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
# 良い例: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や手動での設定を使います。
これにより、ロジックのバグや変更に影響されないテストデータを保持できます。

# 悪い例:アプリケーションロジックを使ってテストデータを作成
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を定義します。これにより、共通のテストロジックをまとめることができます。

# 共通のテストケースを定義
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を使って呼び出します。

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よりもモジュールを利用して共通ロジックを共有する方が適していることもあります。特に、メソッドを共有する場合に有効です。

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内にデータを配置
    • テストデータはdescribecontext内に定義し、スコープを広げすぎないようにします。
  • updateを避ける
    • テストデータの作成にはFactoryBotのtraitsやattributesを使い、updateを避けます。
  • アプリケーションロジックを使わない
    • テストデータの作成にはアプリケーションロジックを使用せず、FactoryBotや手動での設定を使います。

shared_examples の使い方

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

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

参考文献

5
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
4