LoginSignup
2
1

【RSpec】先輩のテストの書き方が美しかったので真似できるようにメモ

Posted at

これはなに?

タイトルの通りです。

どんなテスト?

例を3つ作成したので、実際に見て頂いた方が早そうです。

期待値が具体的にある場合

例えば今、以下の関数を作成し、その挙動を確かめるテストを作成するとします。

def some_func(n)
  n + (n + 1)
end

与えられた値と、その次の値を足すだけの単純な関数ですね。

これをテストしましょう。

describe '#some_func' do
  # テストケースの配列を定義 ([入力値, 期待される出力])
  test_cases = [
    [1, 3],  # 1 + (1 + 1) = 3
    [2, 5],  # 2 + (2 + 1) = 5
    [5, 11], # 5 + (5 + 1) = 11
    [10, 21], # 10 + (10 + 1) = 21
    [100, 201] # 100 + (100 + 1) = 201
  ]

  test_cases.each do |input, expected|
    context "when n is #{input}" do
      it "returns #{expected}" do
        result = some_func(input)
        expect(result).to eq(expected)
      end
    end
  end
end

このテストでは、test_cases配列に入力値と期待される出力のペアを格納します。
そして、eachメソッドを使ってこの配列をループし、各テストケースでsome_func関数を呼び出し、結果が期待通りであるかどうかを検証します。

contextブロックは、各テストケースで何がテストされているのかを簡潔に記述するために使用されます。
itブロック内では、実際の関数呼び出しの結果が期待値に一致しているかをexpect(result).to eq(expected)を通じて検証します。

true, falseを検証する場合

次の例は、「入力した文字列が10文字以内の半角英字であること」を確認するバリデーション関数を作成しました。

def validate_string(input)
  input.is_a?(String) && input.match?(/\A[a-zA-Z]{1,10}\z/)
end

この関数は、条件とマッチしていたらtrue, 不一致の場合はfalseを返します。

describe '#validate_string' do
  # テストケースを定義 ([入力値, 期待される結果(true/false)])
  test_cases = [
    ['abc', true],              # 3文字の英字
    ['a' * 10, true],           # ちょうど10文字の英字
    ['a' * 11, false],          # 11文字の英字(範囲外)
    ['ABC123', false],          # 数字を含む
    ['A_BC', false],            # アンダースコアを含む
    ['', false]                 # 空文字
  ]

  test_cases.each do |input, expected|
    it "returns #{expected} when input is '#{input}'" do
      result = validate_string(input)
      expect(result).to eq(expected)
    end
  end
end

このテストコードでは、関数の戻り値がtrueまたはfalseであるため、期待される結果もそれに応じて変更されています。
test_cases配列に各テストケースを定義し、eachでループして各入力値に対するvalidate_string関数の結果が期待どおりかどうかを検証します。

itブロックでは、テストの説明を動的に生成しています。これにより、どのテストケースであっても、その具体的な内容がテストの出力に明確に表示され、デバッグが容易になります。

複数の入力値が存在する場合

続いて、与えた文字列がすべて完全一致していたらtrueを返す関数を作成します。

def strings_match?(str1, str2, str3)
  str1 == str2 && str2 == str3
end

この場合、テストケースを入力値部分と結果部分に分けましょう。

describe '#strings_match?' do
  # テストケースを定義 ([入力値の配列, 期待される結果])
  test_cases = [
    [["apple", "apple", "apple"], true],
    [["banana", "banana", "apple"], false],
    [["orange", "banana", "cherry"], false],
    [["cherry", "cherry", ""], true]
  ]

  test_cases.each do |inputs, expected|
    it "returns #{expected} when inputs are #{inputs.join(', ')}" do
      result = strings_match?(*inputs)
      expect(result).to eq(expected)
    end
  end
end

eachメソッドに与えられるブロックの中の変数を少なくすることで、テストケースの可読性が向上します。

なぜ美しいのか?

説明するまでもなく、感覚として美しいと感じるこのテスト。
わざわざ言語化してみます。

  • 短いコードで複数のテストを実行できる
  • テストケースが分かりやすい
  • テストの説明や結果を動的に生成している
  • やってることは単純(技術自体は初学者レベル)

しびれますね。

注意点

ただし、何でもかんでもこの方式のテストが正解という訳では無さそうです。
一応注意点も記載します。

パフォーマンス

大量のテストケースをeachで回す場合、テストの実行時間が問題になることがあります。
特に、重いセットアップ処理を伴うテストや外部システムとの連携が必要なテストでは、性能に配慮することが重要です。

テストの独立性

各テストケースが互いに独立していることを確保することが重要です。
1つのテストケースの結果が別のテストケースに影響を与えないようにしましょう。
これには、適切なセットアップやティアダウンの処理が含まれます。

2
1
1

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
2
1