はじめに
こんにちは、エンジニア3年目の嶋田です。
この記事を開いていただきありがとうございます!
今回の記事では、RSpecの基本的な構文と概念についてまとめました。
最近の業務ではRSpecを使用していますが、
どういうコードが読みやすく綺麗であるのかを最近意識するようになってきました。
(気にするのが遅いですし、まだまだですが…)
その中で、理解しておくといいなと思ったことを自分自身のために備忘録としてまとめました。
よろしければ、最後までお付き合い下さい。
目次
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
の説明を読むことでテストの概要がわかることが理想的です。
例えば以下のようなコードです。
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! の違い
let
とlet!
はテストデータを準備するための方法ですが、評価タイミングが異なります。
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!
を使用することで、テストデータが確実に生成され、テストの実行順序によって予期しない結果が発生することを防ぐことがあります。
上書きは避ける
そして!
let
やlet!
の上書きは避けたほうが良いです。上書きをすると定義箇所を追跡するのが難しくなり、テストの可読性が低下します。上書きが必要になる場合は、describe
やcontext
の分割を見直すべきかもしれません。
以下は、let
やlet!
を上書きしている例です。
これにより定義箇所を追跡するのが難しくなり、テストの可読性が低下します。
# 悪い例:上書きしている
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!
を使用したほうがテストデータが明確になります。
テストデータと変数の管理
テストデータや変数を適切に管理することがコードの可読性を向上させます。
必要な箇所でローカル変数を使用する
let
やlet!
である必要がないものは、ローカル変数を基本的に使用します。
ローカル変数はスコープが狭いため、読みやすく、意図が明確になります。
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!
の上書きが起きてしまうことになりがちがだからです。
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
を使用しましょう!
-
適切なスコープで使用する
- 必要以上に広い範囲で使わず、関連するテストケースだけで使用します。
-
ファイルを跨いで共有しない
- 同じファイル内で使用し、関連するテストが一緒に管理されるようにします。
-
適切な命名を行う
-
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内にデータを配置
- テストデータは
describe
やcontext
内に定義し、スコープを広げすぎないようにします。
- テストデータは
-
updateを避ける
- テストデータの作成にはFactoryBotのtraitsやattributesを使い、updateを避けます。
-
アプリケーションロジックを使わない
- テストデータの作成にはアプリケーションロジックを使用せず、FactoryBotや手動での設定を使います。
shared_examples の使い方
-
shared_examples
- 複数のテストケースで共通するテストコードを再利用するための機能です。
-
使用の注意点
- 適切なスコープで使用し、ファイルを跨いで共有しない、適切な命名を行うことが重要です。
ここまで読んでいただきありがとうございます。
let!
激推しの記事も読みましたし、読みやすいコードが人それぞれ違うようで難しいですね…
この記事での紹介はあくまでも1つの方法として捉えていただければありがたいです。
そして、私自身まだまだ勉強中なので、もしもっと良い方法があればぜひ教えていただきたいです。
参考文献