【保存版】初心者のためのRSpec入門!書き方・使い方を丁寧に解説

当サイトでは一部リンクに広告が含まれています

この記事を読んでいる方は 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メソッドの活用

describeitexpectメソッドを組み合わせて使用することで、テストケースを詳細に記述することができます。

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 numberswhen 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_doubleCalculatorクラスのインスタンスではなく、ダブルです。

オブジェクトのダブルによる置き換え

モックを作成すると、テスト対象のオブジェクトをモックオブジェクトに置き換えることができます。

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

 

目次