この記事を読んでいる方は Ruby、または Ruby on Rails に興味を持っている方だと思いますが、実務経験者を除き、テストの経験豊富な方は少ないかと思います。
今回は、Rubyの世界でよく使われるテストフレームワーク、「RSpec」を使ったテストの基本を学びましょう。
なぜRSpecが重要なのか、そして具体的にどのように使えば良いのかを、分かりやすい例とともに解説します。
イントロダクション
テストの重要性と種類:ユニットテストとE2Eテスト
プログラムを作っていく上で、「テスト」は必ず必要になってきます。
テストがあることで、私たちはプログラムが想定通りに動いていることを確認することができます。
テストはたくさんの種類がありますが、今回は「ユニットテスト」と「E2Eテスト」について解説します。
ユニットテストとは、プログラムの一部分(「ユニット」または「部品」)が正しく動くかを確認するテストです。それに対して、E2Eテストは「End to End」の略で、ユーザーの視点でシステム全体が正しく動作することを確認するテストです。
RSpecとは何か
それでは、具体的にどのようにテストを書くのかというと、Rubyの世界では「RSpec」というツールがよく使われます。RSpecはRubyで書かれたテスト作成のためのフレームワークで、わかりやすいテストコードを書くことができます。RSpecを使えば、自分が作ったプログラムが正しく動くかをチェックし、プログラムの質を上げることができます。
Rubyのインストール
RSpecを使うためには、まずRubyが自分のパソコンにインストールされている必要があります。Rubyは公式ウェブサイトから無料でダウンロードできます。インストール方法はサイトに詳しく書かれているので、それに従ってインストールしましょう。
RSpecのインストール
次に、RSpecをインストールします。コマンドライン(黒い画面で文字を打つところ)で以下のコマンドを打ちましょう。
gem install rspec
これで、RSpecがインストールされます。
RSpecの初期設定:rspec –initコマンド
RSpecを使ってテストを書き始める前に、最初に一度だけ設定をする必要があります。これを「初期化」と言います。コマンドラインでテストを書きたいプロジェクトのフォルダに移動したら、以下のコマンドを打ちましょう。
rspec --init
これで、RSpecの設定ファイルが作られます。これからRSpecを使ってテストを書いていく準備が整いました。次回からはこの設定ファイルを使って、テストを書いていくことができます。
RSpecの実行方法
RSpecのテストを実行するには、コマンドラインで以下のコマンドを実行します。
rspec
RSpecの実行結果
RSpecのテストを実行すると、以下のような結果が表示されます。
$ rspec
.
Finished in 0.001 seconds (files took 0.083 seconds to load)
1 example, 0 failures
上記は、テストが1つ実行され、すべてのテストが成功したことを示しています。
一方、テストが失敗した場合は以下のような結果が表示されます。
$ rspec
F
Failures:
1) Calculator adds two numbers
Failure/Error: expect(calculator.add(2, 3)).to eq(5)
expected: 5
got: 6
(compared using ==)
# ./calculator_spec.rb:5:in `block (2 levels) in <top (required)>'
Finished in 0.001 seconds (files took 0.083 seconds to load)
1 example, 1 failure
Failed examples:
rspec ./calculator_spec.rb:4 # Calculator adds two numbers
上記のように、テストが失敗した場合は、失敗したテストの詳細が表示されます。
RSpecの基本構造
RSpecの基本的な構造について説明します。
以下の要素が主な構成要素となります。
describeメソッドの使用
RSpecでは、テストケースをグループ化するためにdescribe
メソッドを使用します。
describe
メソッドは、引数にグループの名前(文字列またはシンボル)を受け取ります。
describe "Calculator" do
# テストケースを記述
end
itメソッドの使用
テストケースを具体的に記述するためには、it
メソッドを使用します。
it
メソッドは、テストケースの期待される動作を記述します。
it "adds two numbers" do
# テストケースの実装
end
expectメソッドとeqメソッド
RSpecでは、テストケースで期待値と実際の値を比較するためにexpect
メソッドとeq
メソッドを組み合わせて使用します。
eq
メソッドは、2つの値が等しいことを検証します。
expect(actual_value).to eq(expected_value)
describe、it、expectメソッドの活用
describe
、it
、expect
メソッドを組み合わせて使用することで、テストケースを詳細に記述することができます。
describe "Calculator" do
it "adds two numbers" do
calculator = Calculator.new
result = calculator.add(2, 3)
expect(result).to eq(5)
end
end
テスト失敗時の読み解き
テストが失敗した場合、RSpecは失敗の詳細な情報を提供します。
失敗の原因を特定するために、この情報を注意深く読み解くことが重要です。
Example Group内での複数の例
describe
ブロック内には複数のit
ブロックを記述することができます。
これにより、複数のテストケースをグループ化して管理することができます。
describe "Calculator" do
it "adds two numbers" do
# テストケースの実装
end
it "subtracts two numbers" do
# テストケースの実装
end
it "multiplies two numbers" do
# テストケースの実装
end
end
重複の削減
テストコードにおける重複を削減する方法について説明します。
beforeフックとインスタンス変数
RSpecでは、テストケース間で共有する変数やセットアップコードをbefore
フックとインスタンス変数を使用して共有することができます。
before
フックは、各テストケースの前に実行されるコードブロックです。
書き方は以下のようになります。
describe "Calculator" do
before do
# セットアップコード
end
it "adds two numbers" do
# テストケースの実装
end
it "subtracts two numbers" do
# テストケースの実装
end
it "multiplies two numbers" do
# テストケースの実装
end
end
具体例を見てみましょう。
以下のような実装(プロダクトコード)があるとします。
class Calculator
def add(a, b)
a + b
end
def subtract(a, b)
a - b
end
end
この実装に対して、以下のようなテストコードを記述することができます。
describe "Calculator" do
before do
@calculator = Calculator.new
end
it "adds two numbers" do
result = @calculator.add(2, 3)
expect(result).to eq(5)
end
it "subtracts two numbers" do
result = @calculator.subtract(5, 2)
expect(result).to eq(3)
end
end
ヘルパーメソッドの活用
重複を削減するもう一つの方法は、ヘルパーメソッドを使用することです。
ヘルパーメソッドは、共通の処理をまとめて再利用するためのメソッドです。
書き方は以下のようになります。
def メソッド名
# 共通の処理
end
具体例を見てみましょう。
以下のような実装(プロダクトコード)があるとします。
class Calculator
def add(a, b)
a + b
end
def subtract(a, b)
a - b
end
end
この時、create_calculator
メソッドをヘルパーメソッドとして定義することで、テストコードを以下のように記述することができます。
def create_calculator
Calculator.new
end
before do
@calculator = create_calculator
end
it "adds two numbers" do
result = @calculator.add(2, 3)
expect(result).to eq(5)
end
it "subtracts two numbers" do
result = @calculator.subtract(5, 2)
expect(result).to eq(3)
end
変異の問題
テストコードにおいて、変異(mutation)はバグの原因となる可能性があります。
RSpecでは、変異を防ぐためにlet
メソッドを使用することが推奨されています。
letメソッドの活用
let
メソッドは、テストコンテキスト内でのみ有効な一時的な変数を定義するために使用されます。
let
メソッドは、最初に参照された時点で計算され、以降の参照では同じ値が返されます。
基本的な書き方は以下のようになります。
let(:変数名) { 値 }
具体例を見てみましょう。
以下のような実装(プロダクトコード)があるとします。
class Calculator
def add(a, b)
a + b
end
def subtract(a, b)
a - b
end
end
この時、テストの前にcalculator
変数を定義することで、テストコードを以下のように記述することができます。
let(:calculator) { Calculator.new }
it "adds two numbers" do
result = calculator.add(2, 3)
expect(result).to eq(5)
end
it "subtracts two numbers" do
result = calculator.subtract(5, 2)
expect(result).to eq(3)
end
このように、let
メソッドを使用することで、テストコードをより簡潔に記述することができます。
重複の排除
重複を排除することは、テストコードの保守性を高めるために重要です。
重複を見つけた場合は、適切なリファクタリングを行い、コードの再利用性を向上させましょう。
例えば、以下のようなテストコードがあるとします。
describe "Calculator" do
it "adds two numbers" do
calculator = Calculator.new
result = calculator.add(2, 3)
expect(result).to eq(5)
end
it "subtracts two numbers" do
calculator = Calculator.new
result = calculator.subtract(5, 2)
expect(result).to eq(3)
end
it "adds two numbers again" do
calculator = Calculator.new
result = calculator.add(7, 8)
expect(result).to eq(15)
end
end
このテストコードでは、code>@calculator</code変数の初期化処理が重複しています。
このような場合は、before
メソッドを使用して、重複を排除することができます。
describe "Calculator" do
before do
@calculator = Calculator.new
end
it "adds two numbers" do
result = @calculator.add(2, 3)
expect(result).to eq(5)
end
it "subtracts two numbers" do
result = @calculator.subtract(5, 2)
expect(result).to eq(3)
end
it "adds two numbers again" do
result = @calculator.add(7, 8)
expect(result).to eq(15)
end
end
このように、before
メソッドを使用することで、テストコードをより簡潔に記述することができます。
コンテキストとフック
RSpecでは、テストコードをさらに整理するために、コンテキスト(context)とフック(hook)の概念を活用することができます。
以下にコンテキストとフックの使用例を示します。
contextメソッドとネストしたdescribe
context
メソッドは、describe
メソッドと同じようにグループ化を行うために使用されます。
context
メソッドは、特定の状況や条件を表す場合に使用します。
describe "Calculator" do
context "when adding numbers" do
it "returns the sum of two positive numbers" do
# テストケースの実装
end
it "returns zero when adding zero to a number" do
# テストケースの実装
end
end
context "when subtracting numbers" do
it "returns the difference of two positive numbers" do
# テストケースの実装
end
end
end
例えば、上記のようなテストコードがあるとします。
この時、context
メソッドは、describe
メソッドと同じようにグループ化を行うために使用されます。
context
メソッドは、特定の状況や条件を表す場合に使用されます。
上記の例では、context
メソッドは、when adding numbers
とwhen subtracting numbers
という状況を表しています。
(日本語でいうと、「数値を加算する場合」と「数値を減算する場合」のような感じです。)
ネストしたcontext
context
メソッドは、ネストすることができます。
describe "Calculator" do
context "when adding numbers" do
context "with positive numbers" do
it "returns the sum of two positive numbers" do
# テストケースの実装
end
end
context "with negative numbers" do
it "returns the sum of two negative numbers" do
# テストケースの実装
end
end
end
context "when subtracting numbers" do
it "returns the difference of two positive numbers" do
# テストケースの実装
end
end
end
beforeフックとafterフック
before
フックとafter
フックは、各テストケースの前後に実行される共通のコードブロックです。
before
フックはテストの前にセットアップを行うために使用され、after
フックはテストの後にクリーンアップを行うために使用されます。
describe "Calculator" do
before do
# テストのセットアップ
end
after do
# テストのクリーンアップ
end
it "adds two numbers" do
# テストケースの実装
end
it "subtracts two numbers" do
# テストケースの実装
end
end
単一コンテキストフック
before
フックとafter
フックを各テストケースごとに設定する代わりに、context
ブロックに対して単一のフックを設定することもできます。
このフックは、該当するコンテキスト内のすべてのテストケースの前後に実行されます。
describe "Calculator" do
context "when adding numbers" do
before do
# テストのセットアップ
end
after do
# テストのクリーンアップ
end
it "returns the sum of two positive numbers" do
# テストケースの実装
end
it "returns zero when adding zero to a number" do
# テストケースの実装
end
end
end
マルチコンテキストフック
RSpecでは、複数のコンテキストに対して同じフックを設定することもできます。
これにより、コードの重複を減らすことができます。
describe "Calculator" do
before(:context) do
# 共通のセットアップ
end
after(:context) do
# 共通のクリーンアップ
end
context "when adding numbers" do
it "returns the sum of two positive numbers" do
# テストケースの実装
end
it "returns zero when adding zero to a number" do
# テストケースの実装
end
end
context "when subtracting numbers" do
it "returns the difference of two positive numbers" do
# テストケースの実装
end
end
end
具体例を示します。
describe "Calculator" do
before(:context) do
@calculator = Calculator.new
end
context "when adding numbers" do
it "returns the sum of two positive numbers" do
expect(@calculator.add(1, 2)).to eq(3)
end
it "returns zero when adding zero to a number" do
expect(@calculator.add(1, 0)).to eq(1)
end
end
context "when subtracting numbers" do
it "returns the difference of two positive numbers" do
expect(@calculator.subtract(5, 3)).to eq(2)
end
end
end
ここでは、before
フックを使って、Calculator
クラスのインスタンスを作成しています。
このインスタンスは、context
ブロック内のすべてのテストケースで使用できます。
ネストロジック
コンテキストとフックをネストすることで、より複雑なテストシナリオを表現することができます。
このネストロジックを使って、テストの構造を意味的に組織化することができます。
describe "Calculator" do
context "when working with positive numbers" do
before do
# セットアップ
end
it "adds two numbers" do
# テストケースの実装
end
context "and subtracting numbers" do
before do
# 追加のセットアップ
end
it "returns the difference of two numbers" do
# テストケースの実装
end
end
end
end
ここでは、context
ブロックをネストして、テストケースをより意味のまとまりごとに組織化しています。
let変数の上書き
ネストしたコンテキスト内で同じ名前のlet
変数を使用する場合、内側のlet
変数が外側のlet
変数を上書きします。
describe "Calculator" do
let(:number) { 5 }
context "when working with positive numbers" do
let(:number) { 10 }
it "doubles the number" do
expect(number * 2).to eq(20)
end
end
end
テストケースの共有方法
RSpecでは、テストケースを共有して再利用するための機能が提供されています。
以下にテストケースの共有方法を紹介します。
subjectとは
RSpecでは、subject
という特別なメソッドを使用して、テスト対象のオブジェクトや値を指定します。
subject
を使用することで、テストケース内で簡潔に記述することができます。
具体例を示します。
describe Calculator do
subject { Calculator.new }
it "adds two numbers" do
expect(subject.add(2, 3)).to eq(5)
end
end
ここでは、subject
メソッドを使用して、Calculator
クラスのインスタンスを指定しています。
このsubject
メソッドを使用することで、テストケース内でCalculator
クラスのインスタンスをsubject
として参照することができます。
described_class
described_class
は、現在のdescribe
ブロックで定義されているクラスを参照するための特殊なキーワードです。
described_class
を使用することで、テストコードがリファクタリングされた場合でも正しいクラスが参照されます。
describe Calculator do
subject { described_class.new }
it "adds two numbers" do
expect(subject.add(2, 3)).to eq(5)
end
end
ここでは、subject
メソッドの引数にdescribed_class
を指定しています。
このdescribed_class
を使用することで、テストコードがリファクタリングされた場合でも正しいクラスが参照されます。
例えば、Calculator
クラスがCalculator::Basic
クラスとCalculator::Scientific
クラスに分割された場合でも、subject
メソッドの引数にdescribed_class
を指定することで、Calculator
クラスのインスタンスが参照されます。
このように、described_class
を使用することで、テストコードがリファクタリングされた場合でも正しいクラスが参照されるようになります。
shared_examplesによる共有
shared_examples
を使用すると、テストケースを共有するためのコードブロックを定義できます。
shared_examples
内で定義されたテストケースは、it_behaves_like
を使用して他のdescribe
ブロックから参照することができます。
shared_examples "addition" do
it "adds two numbers" do
expect(subject.add(2, 3)).to eq(5)
end
end
describe Calculator do
subject { Calculator.new }
it_behaves_like "addition"
end
上記を詳しく読み解いてみましょう。
まず、shared_examples
メソッドを使用して、addition
という名前のテストケースを定義しています。
このshared_examples
メソッドは、it_behaves_like
メソッドを使用して他のdescribe
ブロックから参照することができます。
次に、describe
ブロック内でsubject
メソッドを使用して、Calculator
クラスのインスタンスを指定しています。
このsubject
メソッドを使用することで、テストケース内でCalculator
クラスのインスタンスをsubject
として参照することができます。
最後に、it_behaves_like
メソッドを使用して、addition
という名前のテストケースを参照し、テストケースを実行しています。
shared_examples_forとit_behaves_like
shared_examples_for
は、複数のit_behaves_like
ブロックをまとめて定義するためのメソッドです。
これにより、テストケースの共有がさらにシンプルになります。
shared_examples_for "addition and subtraction" do
it "adds two numbers" do
expect(subject.add(2, 3)).to eq(5)
end
it "subtracts two numbers" do
expect(subject.subtract(5, 2)).to eq(3)
end
end
describe Calculator do
subject { Calculator.new }
it_behaves_like "addition and subtraction"
end
この場合、it_behaves_like
メソッドは、addition and subtraction
という名前のテストケースを参照し、テストケースを実行できます。
shared_contextによる共有
shared_context
を使用すると、テストコンテキストを共有することができます。
shared_context
内で定義された変数やメソッドは、include_context
を使用して他のdescribe
ブロックから参照することができます。
shared_context "calculator context" do
let(:calculator) { Calculator.new }
end
describe Calculator do
include_context "calculator context"
it "adds two numbers" do
expect(calculator.add(2, 3)).to eq(5)
end
end
ここでは、shared_context
メソッドを使用して、calculator context
という名前のテストコンテキストを定義しています。
shared_examples
と同様に、shared_context
メソッドは、include_context
メソッドを使用して他のdescribe
ブロックから参照することができます。
shared_examples
と似ていますが、shared_context
はテストケースを定義するためのものではなく、テストコンテキスト(変数など)を定義するためのものです。
ワンライナー記法の例
RSpecでは、テストケースをワンライナー形式で記述することもできます。
ワンライナー記法は短くて簡潔な記述ができるため、テストコードの可読性を向上させることができます。
describe Calculator do
it { is_expected.to respond_to(:add).with(2).arguments }
it { expect(subject.add(2, 3)).to eq(5) }
end
上記のコードは、以下のコードと同じ意味になります。
describe Calculator do
it "responds to add with 2 arguments" do
expect(subject).to respond_to(:add).with(2).arguments
end
it "adds two numbers" do
expect(subject.add(2, 3)).to eq(5)
end
end
もちろん、どちらでも同じ結果になるのですが、ワンライナー記法の方が短くて簡潔なので、可読性が向上する場合があります。
include_contextによる共有コンテキスト
include_context
を使用すると、複数のテストケースで同じコンテキストを共有することができます。
shared_context "calculator context" do
let(:calculator) { Calculator.new }
end
describe Calculator do
include_context "calculator context"
it "adds two numbers" do
expect(calculator.add(2, 3)).to eq(5)
end
end
上記のコードでは、include_context
メソッドを使用して、calculator context
という名前のテストコンテキストを参照しています。
組み込みマッチャ
RSpecでは、組み込みのマッチャを使用してテストの検証を行います。
以下にいくつかの一般的な組み込みマッチャの例を示します。
マッチャとは
マッチャは、テストケースの期待値と実際の値を比較するために使用されます。
RSpecでは、様々な種類のマッチャを提供しています。
等価性マッチャ(eq)
eq
マッチャは、2つの値が等しいことを検証します。
eql
マッチャは、2つの値が同じ型であり、かつ等しいことを検証します。
expect(result).to eq(5)
expect(result).to eql(5)
具体例:
describe Calculator do
subject { Calculator.new }
it "adds two numbers" do
# 2 + 3 が 5 と等しいことを検証する
expect(subject.add(2, 3)).to eq(5)
end
end
等価性マッチャ(equalとbe)
equal
マッチャは、2つのオブジェクトが同じオブジェクトであることを検証します。
be
マッチャは、equal
マッチャと同じです。
expect(result).to equal(5)
expect(result).to be(5)
具体例:
describe Calculator do
subject { Calculator.new }
it "adds two numbers" do
# 2 + 3 が 5 と等しいことを検証する
expect(subject.add(2, 3)).to be(5)
end
end
`
not_toメソッド
not_to
メソッドは、期待値が実際の値と等しくないことを検証します。
テストが成功する場合は、期待値と実際の値が異なることを意味します。
expect(result).not_to eq(5)
具体例:
describe Calculator do
subject { Calculator.new }
it "adds two numbers" do
# 2 + 3 が 5 と等しくないことを検証する
expect(subject.add(2, 3)).not_to eq(6)
end
end
比較マッチャ
比較マッチャは、数値や文字列の大小関係を検証するために使用されます。
expect(result).to be > 5
expect(result).to be >= 5
expect(result).to be < 10
expect(result).to be <= 10
具体例:
describe Calculator do
subject { Calculator.new }
it "adds two numbers" do
# 2 + 3 が 5 より大きいことを検証する
expect(subject.add(2, 3)).to be > 5
end
end
等価性と比較マッチャ
等価性と比較マッチャを組み合わせて使用することもできます。
expect(result).to be > 5
expect(result).to be_between(1, 10).inclusive
expect(result).to be_within(0.5).of(5.0)
具体例:
describe Calculator do
subject { Calculator.new }
it "adds two numbers" do
# 2 + 3 が 5 以上 10 以下であることを検証する
expect(subject.add(2, 3)).to be_between(5, 10).inclusive
end
end
述語(predicate)マッチャ
述語マッチャは、メソッドが真偽値を返すことを検証します。
expect(result).to be_odd
expect(result).to be_even
具体例:
describe Calculator do
subject { Calculator.new }
it "adds two numbers" do
# 2 + 3 が奇数であることを検証する
expect(subject.add(2, 3)).to be_odd
end
end
allマッチャ
all
マッチャは、配列のすべての要素が特定の条件を満たすことを検証します。
expect(array).to all(be_positive)
expect(array).to all(be_an(Integer))
allマッチャの具体例:
describe Array do
subject { [1, 2, 3] }
it "has all positive numbers" do
# 配列のすべての要素が正の数であることを検証する
expect(subject).to all(be_positive)
end
end
beマッチャ(真偽値、nil)
be
マッチャは、真偽値やnil
を検証するために使用されます。
expect(result).to be(true)
expect(result).to be(false)
expect(result).to be(nil)
具体例:
describe BeTest do
subject { BeTest.new }
it "returns true" do
# true を返すことを検証する
expect(subject.true_value).to be(true)
end
it "returns false" do
# false を返すことを検証する
expect(subject.false_value).to be(false)
end
it "returns nil" do
# nil を返すことを検証する
expect(subject.nil_value).to be(nil)
end
end
predicate、all、beマッチャ
これらのマッチャを組み合わせることで、柔軟な検証を行うことができます。
expect(result).to be_truthy
expect(array).to all(be_an(Integer).and(be_positive))
expect(result).to be_a(String).and(start_with("Hello"))
具体例:
describe MultipleMatchers do
subject { MultipleMatchers.new }
it "returns a string starting with 'Hello'" do
# 文字列であり、"Hello" で始まることを検証する
expect(subject.greeting).to be_a(String).and(start_with("Hello"))
end
it "returns a string containing the user's name" do
# 文字列であり、ユーザーの名前を含むことを検証する
expect(subject.greeting).to be_a(String).and(include("John"))
end
it "returns an array starting with 'Hello'" do
# 配列であり、"Hello" で始まることを検証する
expect(subject.greetings).to be_an(Array).and(start_with("Hello"))
end
it "returns an array containing the user's name" do
# 配列であり、ユーザーの名前を含むことを検証する
expect(subject.greetings).to be_an(Array).and(include("John"))
end
end
このように、複雑な検証を行うことができます。
changeマッチャ
change
マッチャは、メソッドの呼び出しによって値が変化することを検証します。
expect { object.method }.to change(object, :attribute).from(old_value).to(new_value)
expect { object.method }.to change { object.attribute }.from(old_value).to(new_value)
具体例:
describe ChangeTest do
subject { ChangeTest.new }
it "changes the value of the attribute" do
# メソッドの呼び出しによって属性の値が変化することを検証する
expect { subject.change_value }.to change(subject, :value).from(1).to(2)
end
it "changes the value of the attribute using a block" do
# メソッドの呼び出しによって属性の値が変化することを検証する
expect { subject.change_value }.to change { subject.value }.from(1).to(2)
end
end
change
マッチャは、例えば、メソッドの呼び出しによってデータベースのレコードが作成されることを検証するために使用することができます。
具体的には、レコード数が増えることを検証できます。
expect { object.method }.to change(Model, :count).by(1)
contain_exactlyマッチャ
contain_exactly
マッチャは、配列が指定された要素を正確に含むことを検証します。
expect(array).to contain_exactly(1, 2, 3)
expect(array).to contain_exactly(*expected_array)
具体例:
describe Array do
subject { [1, 2, 3] }
it "contains exactly the elements" do
# 配列が指定された要素を正確に含むことを検証する
expect(subject).to contain_exactly(1, 2, 3)
end
end
start_withとend_withマッチャ
start_with
マッチャは、文字列や配列が指定された要素で始まることを検証します。
end_with
マッチャは、文字列や配列が指定された要素で終わることを検証します。
expect(string).to start_with("Hello")
expect(array).to start_with(1, 2, 3)
expect(string).to end_with("World")
expect(array).to end_with(3, 4, 5)
具体例:
describe StartEndWith do
subject { "Hello World" }
it "starts with 'Hello'" do
# 文字列が "Hello" で始まることを検証する
expect(subject).to start_with("Hello")
end
it "starts with 'Hello' and ends with 'World'" do
# 文字列が "Hello" で始まり、"World" で終わることを検証する
expect(subject).to start_with("Hello").and end_with("World")
end
end
have_attributesマッチャ
have_attributes
マッチャは、オブジェクトが指定された属性を持つことを検証します。
expect(object).to have_attributes(name: "John", age: 30)
具体例:
describe HaveAttributes do
subject { HaveAttributes.new }
it "has the specified attributes" do
# オブジェクトが指定された属性を持つことを検証する
expect(subject).to have_attributes(name: "John", age: 30)
end
it "has the specified name" do
# オブジェクトが指定された名前を持つことを検証する
expect(subject).to have_attributes(name: "John")
end
end
includeマッチャ
include
マッチャは、配列や文字列が指定された要素を含むことを検証します。
expect(array).to include(1, 2, 3)
具体例:
describe Include do
subject { [1, 2, 3] }
it "includes the specified elements" do
# 配列が指定された要素を含むことを検証する
expect(subject).to include(1)
end
end
raise_errorマッチャ
raise_error
マッチャは、特定のエラーが発生することを検証します。
expect { raise StandardError }.to raise_error(StandardError)
expect { object.method }.to raise_error(CustomError)
具体例:
describe RaiseError do
subject { RaiseError.new }
it "raises an error" do
# エラーが発生することを検証する
expect { subject.raise_error }.to raise_error(StandardError)
end
it "raises a custom error" do
# カスタムエラーが発生することを検証する
expect { subject.raise_custom_error }.to raise_error(CustomError)
end
end
これは、例えば例外が発生することを検証するために使用することができます。
respond_toマッチャ
respond_to
マッチャは、オブジェクトが指定されたメソッドを持つことを検証します。
# オブジェクトが指定されたメソッドを持つことを検証する
expect(object).to respond_to(:method)
# オブジェクトが指定されたメソッドを持ち、引数を1つ取ることを検証する
expect(object).to respond_to(:method).with(1).argument
具体例:
describe RespondTo do
subject { RespondTo.new }
it "responds to the specified methods" do
# オブジェクトが指定されたメソッドを持つことを検証する
expect(subject).to respond_to(:method)
end
it "responds to the specified methods with arguments" do
# オブジェクトが指定されたメソッドを持ち、引数を1つ取ることを検証する
expect(subject).to respond_to(:method).with(1).argument
end
end
複合的な期待値(expectations)
RSpecでは、マッチャを組み合わせて複合的な期待値を構築することもできます。
expect(result).to be > 5
expect(result).to be_between(1, 10).inclusive
expect(result).to be_within(0.5).of(5.0)
expect(array).to all(be_an(Integer).and(be_positive))
expect(result).to be_a(String).and(start_with("Hello"))
モック
モックとは
モックは、テスト中にテスト対象のオブジェクトとのやり取りを模倣するオブジェクトです。
モックは、依存関係のあるオブジェクトがまだ実装されていない場合や、外部リソースにアクセスする必要がある場合に特に有用です。
例えば、映画の評価を計算するMovieRatingCalculator
クラスがあるとします。
class MovieRatingCalculator
def initialize(movie)
@movie = movie
end
def calculate
# 映画の評価を計算する
end
end
このクラスは、映画の評価を計算するために、Movie
オブジェクトを必要とします。
このMovie
オブジェクトは、MovieDatabase
クラスを使用してデータベースから取得する必要があります。
class MovieDatabase
def find(id)
# データベースから映画を取得する
end
end
この場合、MovieRatingCalculator
クラスのテストでは、MovieDatabase
クラスのインスタンスを作成する必要があります。
describe MovieRatingCalculator do
subject { MovieRatingCalculator.new(movie) }
let(:movie) { MovieDatabase.new.find(1) }
it "calculates the movie rating" do
# 映画の評価を計算する
expect(subject.calculate).to eq(5)
end
end
しかし、このテストでは、MovieDatabase
クラスのテストではなく、MovieRatingCalculator
クラスのテストを行っていることになりますよね。
このような場合に、モックを使用することで、MovieDatabase
クラスのテストを回避することができます。
もちろん、MovieDatabase
クラスのテストも行う必要がありますが、それは別のテストで行うことができます。
テストのスコープ(範囲)を狭めることで、よりテストをシンプルにすることができます。
テストダブルの作成
モックを作成するには、double
メソッドを使用します。
calculator_mock = double("calculator")
double
メソッドの引数には、モックの名前を指定します。
具体的な使い方についてはこの後、見ていきましょう。
ダブル
モックは、テスト中にテスト対象のオブジェクトのメソッド呼び出しをシミュレートするために使用されます。
モックを使用することで、テスト対象のオブジェクトの振る舞いをコントロールすることができます。
expect(calculator_mock).to receive(:add).with(2, 3).and_return(5)
モックとダブルの違うところは、モックはテスト対象のオブジェクトの振る舞いをコントロールすることができるのに対して、ダブルはテスト対象のオブジェクトの振る舞いをコントロールすることができないという点です。
expect(calculator_double).to receive(:add).with(2, 3).and_return(5)
上記の例では、calculator_double
はCalculator
クラスのインスタンスではなく、ダブルです。
オブジェクトのダブルによる置き換え
モックを作成すると、テスト対象のオブジェクトをモックオブジェクトに置き換えることができます。
let(:calculator_mock) { double("calculator") }
before do
allow(Calculator).to receive(:new).and_return(calculator_mock)
end
上記の例では、Calculator
クラスのインスタンスを作成する際に、calculator_mock
を返すように設定しています。
受け取り回数の確認
モックオブジェクトに対してメソッド呼び出しを期待する際、その呼び出し回数も指定することができます。
expect(calculator_mock).to receive(:add).with(2, 3).exactly(3).times
allowメソッド
先ほどの説明で登場しましたが、allow
メソッドを使用すると、モックオブジェクトが特定のメソッド呼び出しを許可するように指定することができます。
allow(calculator_mock).to receive(:add).and_return(5)
引数のマッチング
モックオブジェクトに対してメソッド呼び出しを期待する際、引数の値を特定の条件でマッチングすることができます。
expect(calculator_mock).to receive(:add).with(an_instance_of(Integer), be > 0).and_return(5)
インスタンスダブル
モックオブジェクトに加えて、RSpecではインスタンスダブルを使用することもできます。
インスタンスダブルは、特定のクラスのインスタンスをシミュレートするために使用されます。
calculator_double = instance_double(Calculator, add: 5)
上記の例では、Calculator
クラスのインスタンスをシミュレートするためのインスタンスダブルを作成しています。
例えば、Calculator
クラスのインスタンスに対してadd
メソッドを呼び出すと、5
を返すように設定しています。
クラスダブル
クラスダブルは、特定のクラスをシミュレートするために使用されます。
calculator_class_double = class_double(Calculator, new: calculator_double)
上記の例では、Calculator
クラスをシミュレートするためのクラスダブルを作成しています。
例えば、Calculator
クラスのインスタンスを作成すると、先ほど作成したインスタンスダブルを返すように設定しています。
スパイ
スパイは、モックと同様にメソッド呼び出しを監視するために使用されますが、スパイは実際の実装を使用する点が異なります。
calculator_spy = spy("calculator")
expect(calculator_spy).to receive(:add).with(2, 3).and_return(5)
上記の例では、calculator_spy
というスパイを作成しています。
スパイは、モックと同様にメソッド呼び出しを監視することができます。
モック・ダブル・スパイの比較
モック・ダブル・スパイの違いをまとめた表を以下に示します。
目的 | メソッド呼び出しの検証 | 戻り値の設定 | メソッド呼び出し回数の検証 | |
---|---|---|---|---|
モック | 他のオブジェクトとの対話をシミュレート | 必須 | 必須 | 必須 |
ダブル | 他のオブジェクトの振る舞いをシミュレート | 任意 | 必須または任意 | 任意 |
スパイ | 他のオブジェクトのメソッド呼び出しを監視 | 必須 | 任意 | 必須または任意 |
モックは他のオブジェクトとの対話をシミュレートし、メソッド呼び出しや引数の検証、戻り値の設定、メソッド呼び出し回数の検証を行います。
ダブルは他のオブジェクトの振る舞いをシミュレートし、メソッド呼び出しや戻り値の設定を行います。メソッド呼び出しの検証やメソッド呼び出し回数の検証は任意です。
スパイは他のオブジェクトのメソッド呼び出しを監視し、メソッド呼び出しや引数の検証、メソッド呼び出し回数の検証を行います。戻り値の設定は任意です。
これらのテストダブルは、テスト駆動開発や振る舞い駆動開発のプラクティスにおいて、依存関係や外部リソースとのやり取りを制御し、テストの安定性と可読性を向上させるために重要なツールとなります。
Ruby on Railsでのテスト
Ruby on Railsでも RSpec を使用してテストを行うことができます。
ここでは、
RSpecのインストールと設定
最初にRSpecをインストールします。RailsのプロジェクトのGemfileに以下を追記します。
group :development, :test do
gem 'rspec-rails', '~> 5.0'
end
そして、ターミナルでbundle install
を実行します。これでRSpecのRails用ライブラリがインストールされます。
次に、RSpecの設定を行います。ターミナルでrails generate rspec:install
を実行し、RSpecの初期設定を行います。このコマンドを実行すると、spec
ディレクトリといくつかの設定ファイルが作成されます。
Capybaraのインストールと設定
次にCapybaraをインストールします。Gemfileに以下を追記し、bundle install
を実行します。
group :test do
gem 'capybara'
gem 'webdrivers'
end
Capybaraを設定するために、spec/rails_helper.rb
ファイルの末尾に以下の行を追加します。
require 'capybara/rspec'
統合テストの例
Capybara を用いたテストの例を見てみましょう。
spec/system/posts_spec.rb (仮のコード)
require 'rails_helper'
RSpec.describe 'Posts', type: :system do
let(:user) { create(:user) } # テストユーザーを作成
before do
# ログインする
visit login_path
fill_in 'Email', with: user.email
fill_in 'Password', with: user.password
click_button 'Log in'
end
it '新規投稿を作成する' do
# 新規投稿ページに遷移する
visit new_post_path
fill_in 'Title', with: 'New Post'
fill_in 'Content', with: 'Lorem ipsum dolor sit amet.'
click_button 'Create Post'
# 投稿が成功したことを検証する
expect(page).to have_content 'Post was successfully created.'
expect(page).to have_content 'New Post'
end
end
このコードではまず、let(:user) { create(:user) }
でテスト用のユーザーを作成しています。また、before
ブロック内でテストユーザーをログインさせています。
次に、it 'creates a new post' do
から始まるブロック内で新規投稿を作成するシナリオを記述しています。新規投稿ページに遷移し、タイトルと内容を入力した後に投稿ボタンを押すという一連の操作をCapybaraのメソッドを使ってシミュレートしています。
最後に、expect(page).to have_content
を使って投稿が成功したことを確認するメッセージが表示されているか、そして新しい投稿のタイトルが表示されているかを検証しています。
テストを実行するには、ターミナルでbundle exec rspec spec/system/posts_spec.rb
を実行します。テストが成功すれば、新規投稿の作成のシナリオが正常に動作していることが確認できます。
フォームのテスト
フォームのテストでは、フォームに入力を行い、その結果を検証します。以下に具体的なコードを示します。
fill_in "Title", with: "New Post" # タイトルフィールドに"New Post"と入力します。
fill_in "Content", with: "Lorem ipsum dolor sit amet." # コンテンツフィールドに"Lorem ipsum dolor sit amet."と入力します。
click_button "Create Post" # "Create Post"ボタンをクリックします。
expect(page).to have_content("Post was successfully created.") # 成功メッセージが表示されていることを確認します。
expect(page).to have_content("New Post") # 入力したタイトルが表示されていることを確認します。
これにより、フォームが正しく動作しているか、期待通りの結果が得られているかを確認できます。
具体例:
require 'rails_helper'
RSpec.describe 'Posts', type: :system do
let(:user) { create(:user) }
before do
# ログインする
visit login_path
fill_in 'Email', with: user.email
fill_in 'Password', with: user.password
click_button 'Log in'
end
it '新規投稿に成功する' do
# 新規投稿ページに移動する
visit new_post_path
fill_in 'Title', with: 'New Post'
fill_in 'Content', with: 'Lorem ipsum dolor sit amet.'
click_button 'Create Post'
# 投稿が成功したことを検証する
expect(page).to have_content 'Post was successfully created.'
expect(page).to have_content 'New Post'
end
it '投稿の編集に成功する' do
# 投稿を作成する
post = create(:post, user: user)
# 投稿を編集する
visit edit_post_path(post)
fill_in 'Title', with: 'Updated Post'
fill_in 'Content', with: 'Updated content'
click_button 'Update Post'
# 投稿が更新されたことを検証する
expect(page).to have_content 'Post was successfully updated.'
expect(page).to have_content 'Updated Post'
end
it '投稿の削除に成功する' do
# 投稿を作成する
post = create(:post, user: user)
# 投稿を削除する
visit posts_path
click_link 'Destroy'
# 投稿が削除されたことを検証する
expect(page).to have_content 'Post was successfully destroyed.'
expect(page).not_to have_content post.title
end
end
上記の例では、新規投稿の作成、投稿の編集、投稿の削除の3つのシナリオをテストしています。
it 'edits a post' do
から始まるブロックでは、post = create(:post, user: user)
でテスト用の投稿を作成しています。
visit edit_post_path(post)
で投稿の編集ページに遷移し、タイトルと内容を入力した後に更新ボタンを押すという一連の操作をCapybaraのメソッドを使ってシミュレートしています。
最後に、expect(page).to have_content
を使って投稿が成功したことを確認するメッセージが表示されているか、そして新しい投稿のタイトルが表示されているかを検証しています。
リンクとリダイレクトのテスト
リンクとリダイレクトのテストでは、リンクをクリックした結果、期待通りのページに遷移するかを検証します。
click_link "New Post" # "New Post"リンクをクリックします。
expect(page).to have_current_path(new_post_path) # 新しい投稿のページにリダイレクトされていることを検証します。
これにより、アプリケーションのリンクが正しく機能しているかを確認できます。
具体例:
require 'rails_helper'
# リンクが正しく機能しているかをテストする
RSpec.describe 'Links', type: :system do
let(:user) { create(:user) }
it '新規投稿ページに遷移する' do
# ログインする
visit login_path
fill_in 'Email', with: user.email
fill_in 'Password', with: user.password
click_button 'Log in'
# "New Post"リンクをクリックする
click_link 'New Post'
# 新しい投稿のページにリダイレクトされていることを検証する
expect(page).to have_current_path(new_post_path)
end
it 'ログインページにリダイレクトされる' do
visit posts_path
# ログインページにリダイレクトされていることを検証する
expect(page).to have_current_path(login_path)
end
end
モックとスタブの組み合わせ
外部サービスとの連携をテストする場合、通常はモックやスタブを使用して外部サービスの動作を模倣します。
これにより、外部リソースへのアクセスを制御し、テスト速度を向上させることができます。
allow(ExternalService).to receive(:call).and_return(true) # 外部サービスの呼び出しをスタブ化し、常にtrueを返すように設定します。
result = ExternalService.call # スタブ化したメソッドを呼び出します。
expect(result).to be_truthy # 結果がtrueであることを検証します。
具体例:
require 'rails_helper'
# 外部サービスとの連携をテストする
RSpec.describe 'ExternalService', type: :system do
it '外部サービスとの連携をテストする' do
# 外部サービスの呼び出しをスタブ化し、常にtrueを返すように設定する
allow(ExternalService).to receive(:call).and_return(true)
# 外部サービスを呼び出す
result = ExternalService.call
# 結果がtrueであることを検証する
expect(result).to be_truthy
end
end
このように、モックやスタブを使用することで、外部サービスが正しく呼び出されているかを検証できます。
また、外部サービスが正しく呼び出されていることを前提として、アプリケーションの挙動に集中してテストを書くことができます。
リクエストスペックとレスポンススペック
リクエストスペックでは、HTTPリクエストの送信やパラメータの検証を行います。一方、レスポンススペックでは、アクションの結果やビューの検証を行います。
# リクエストスペックの例
post "/posts", params: { post: { title: "New Post", content: "Lorem ipsum dolor sit amet." } } # 新しい投稿を作成するリクエストを送信します。
expect(response).to have_http_status(:created) # レスポンスステータスが201(作成済み)であることを検証します。
# レスポンススペックの例
get "/posts/1" # 投稿の詳細ページにアクセスします。
expect(response.body).to include("New Post") # レスポンスボディに"New Post"が含まれていることを検証します。
具体例:
require 'rails_helper'
# リクエストとレスポンスが正しく機能しているかをテストする
RSpec.describe 'Requests and Responses', type: :system do
let(:user) { create(:user) }
let(:post) { create(:post, user: user) }
it 'shows the post list' do
# ログインする
visit login_path
fill_in 'Email', with: user.email
fill_in 'Password', with: user.password
click_button 'Log in'
# 投稿一覧ページにアクセスする
get "/posts"
# レスポンスステータスが200(成功)であることを検証する
expect(response).to have_http_status(:ok)
end
it 'shows the post' do
# 投稿の詳細ページにアクセスする
get "/posts/#{post.id}"
# レスポンスボディに"New Post"が含まれていることを検証する
expect(response.body).to include("New Post")
end
it 'redirects to the login page' do
# 投稿一覧ページにアクセスする
get "/posts"
# 未ログイン時、レスポンスステータスが302(一時的なリダイレクト)であることを検証する
expect(response).to have_http_status(:found)
end
it 'creates a new post' do
# 新しい投稿を作成するリクエストを送信する
post "/posts", params: { post: { title: "New Post", content: "Lorem ipsum dolor sit amet." } }
# レスポンスステータスが201(作成済み)であることを検証する
expect(response).to have_http_status(:created)
end
end
ビュースペックの書き方
ビュースペックでは、ビュー(画面表示)のテストを行います。
具体的には、ビューファイルが期待通りにレンダリングされるか、または特定のインスタンス変数やヘルパーメソッドが期待通りに動作するかを確認します。
require 'rails_helper'
# ビューのテストを行う
RSpec.describe 'Posts', type: :system do
let(:user) { create(:user) }
let(:post) { create(:post, user: user) }
it 'shows the post' do
# 投稿の詳細ページをレンダリングする
render template: "posts/show", locals: { post: post }
# レンダリングされたビューに"New Post"が含まれていることを検証する
expect(rendered).to match /New Post/
end
end
ただ、実際には他のスペックでビューがテストされることが多いため、ビュースペックはあまり書かれません。
モデルスペックとデータベースの操作
モデルスペックでは、アプリケーションのモデルに対する操作とその結果を検証します。
モデルのメソッド、バリデーション、アソシエーションなどをテストします。
@post = Post.new(title: "New Post", content: "Lorem ipsum dolor sit amet.") # 新しい投稿を作成します。
expect(@post).to be_valid # 投稿が有効であることを検証します。
expect(@post.title).to eq "New Post" # タイトルが正しく設定されていることを検証します。
具体例:
require 'rails_helper'
# モデルのテストを行う
RSpec.describe Post, type: :model do
let(:user) { create(:user) }
# タイトルと内容があれば有効な状態であること
it 'タイトルと内容があれば有効な状態であること' do
# 新しい投稿を作成する
post = Post.new(title: "New Post", content: "てすと")
# 投稿が有効であることを検証する
expect(post).to be_valid
end
it 'タイトルがなければ無効な状態であること' do
# タイトルがない投稿を作成する
post = Post.new(title: nil, content: "てすと")
# タイトルがない投稿が無効であることを検証する
expect(post).to be_invalid
end
it '内容がなければ無効な状態であること' do
# 内容がない投稿を作成する
post = Post.new(title: "New Post", content: nil)
# 内容がない投稿が無効であることを検証する
expect(post).to be_invalid
end
it '重複したタイトルの投稿は無効な状態であること' do
# 重複したタイトルの投稿を作成する
Post.create(title: "New Post", content: "てすと")
post = Post.new(title: "New Post", content: "てすと")
# 重複したタイトルの投稿が無効であることを検証する
expect(post).to be_invalid
end
it 'タイトルが50文字以内であること' do
# 51文字のタイトルの投稿を作成する
post = Post.new(title: "a" * 51, content: "てすと")
# 51文字のタイトルの投稿が無効であることを検証する
expect(post).to be_invalid
end
end