私の考える良いRSpecの書き方

はじめに

イタンジ株式会社の安倍です。今年7キロのダイエットに成功しました。 精算管理くん、家主管理くんの開発を行っています。

私は学生時代のインターンとイタンジにきて4つのプロダクトの開発に携わりました。その過程で自分のRailsのRSpecのコードの書き方に変化があり、昔と比べると読みやすくなった & テストの質も良くなったと感じたのでどんなRSpecを書いてきたのか、今考える良いRSpecの書き方はどのようなものかを今回紹介します。

ユーザー作成のapiのテストをするサンプルコードを元に紹介していきます。作成されたユーザーの情報を返すAPIです。

学生時代

学生時代のインターンではスタートアップの会社で、テストが存在しないプロダクトでした。なので私に与えられた最初の業務は全てのコードのテストを書くことでした。その時は以下のようなコードを書いていました。

describe 'POST create' do
  before do
    @initial_user_count = User.count
    post api_users_path, params:, as: :json
    @body = JSON.parse(response.body)
  end

  context 'success' do
    let(:params) {
      user: {
        email: 'test@example.com',
        password: 'password'
      }
    }

    it 'status: ok' do
      expect(response.status).to eq 200
    end

    it 'response email' do
      expect(@body["email"]).to eq 'test@example.com'
    end

    it 'increases user count by 1' do
      expect(User.count).to eq @initial_user_count + 1
    end
  end
end

テストを書くのが初めてだったので、どう書けば良いか分からず苦戦してた記憶があります。 今見ると、以下2つのポイントがよくないかなと思います

  • 全てのitブロックでリクエストが実行されて、テストの実行時間が長くなる
  • expectの数が増えれば行数がどんどん長くなりそう

イタンジ1年目

配属された最初のプロダクトのコードを参考に以下のようなコードを書いていました。

describe 'POST create' do
  let(:request) { post api_users_path, params:, as: :json }

  context 'success' do
    let(:params) {
      user: {
        email: 'test@example.com',
        password: 'password'
      }
    }

    it_behaves_like 'ok request with body' do
      let(:expected_response_body) do
        {  email: 'test@example.com' }.to_json
      end
    end

    it do
      expect { request }.to change(User, :count).by(1)
    end
  end
end

# 以下は共通でshared_examplesを使用するために別ファイルで定義されている

shared_examples 'match http status' do
  subject { response }

  before { request }

  it { is_expected.to have_http_status(expected_http_status) }
end

shared_examples 'match response body' do
  subject { response.body }

  before { request }

  it { is_expected.to eq expected_response_body }
end

shared_examples 'ok request with body' do
  it_behaves_like 'match http status' do
    let(:expected_http_status) { :ok }
  end

  it_behaves_like 'match response body'
end

shared_examplesを使うようになりました。今回は 'ok request with body'という名前のshared_examplesを使用しています。 request とexpected_response_bodyを定義しておくことで、status codeが200とレスポンスの内容のテストをしてくれます。 shared_examplesを当時知らなかったので、かなりコードの共通化ができるようになると当時は感動しました。

ただ、以下2つのポイントがよくないかなと思います。

  • request とexpected_response_bodyという変数名で定義しておく必要があり、テストの書き方を覚えるのに少し時間がかかる
  • APIリクエストを2回送っているので、テストの実行時間が長くなる

イタンジ3年目

プロダクト移動を経験し、別の上長にアドバイスをいただいて、以下のようなコードになりました。

describe 'POST /api/users' do
  context 'パラメータが正常な時' do
    let!(:params) {
      user: {
        email: 'test@example.com',
        password: 'password'
      }
    }

    it 'ユーザーが作成され、作成されたユーザー情報が返却される' do
      aggregate_failures do
        expect { post '/api/users', params:, as: :json }.to(
          change(User, :count).by(1)
        )
        assert_response_schema_confirm(200)
        expect(response.parsed_body['email']).to eq 'test@example.com'
      end
    end
  end
end

随分変わった気がします。ポイントとしては以下の4つです。

let!を使用する

letの方が遅延評価で、呼び出されるタイミングで実行されるので良いかと以前は思っていたのですが、コードを上から読めないデメリットがあります。なのでlet!を使用することで、即時評価になりテストを上から読むようにしました。

ではlet!にすることで呼び出さなくていいコードを呼び出してしまうのでは?という考えにもなるのですが、個人的には適切にcontextを分けて、let!を使用することで無駄なlet!の呼び出しはなくなるかなという考えです。

contextブロックやitブロックで日本語を使用する

日本語で書いた方がわかりやすいのでは?と上司にアドバイスをいただき、日本語で書くようになりました。確かに日本語で普段コミュニケーションをとる開発チームであれば、英語ではなく日本語で書いた方がより明確に内容を書ける & 英語でどう表現すれば良いか迷わなくて済むと思いました。

aggregate_failuresを使用する

itブロックのたびにAPIリクエストするのもテスト時間が長くなるため、1つのitブロックでAPIリクエストの後にexpectを複数書くようになりました。ただこの書き方だとrubocopのRSpec/MultipleExpectationsルールで警告が出るのと、どのexpectでエラーが出たかわかりません。なので、aggregate_failuresを使用するようになりました。こちらを使用することで、rubocopの警告がなくなるのと複数のexpectの内、どのexpectでエラーが出たか分かるようになります。

committee-railsを使用する

APIのレスポンスに対してはcommittee-railsのgemで使用できるassert_response_schema_confirmメソッドで検証するようになりました。こちらはOpenAPIのスキーマと一致するかをテストしてくれるものです。こちらにより特定のステータスコードの時に期待したスキーマ構造でレスポンスが返ってくるかの確認 & OpenAPIの整合性の維持ができます。

github.com

終わりに

あくまで私の思う良いRSpecの書き方の紹介でした。 昔と比べて内容が大きく変わり、かなりシンプルになったため以下のポイントが改善したと思います。

  • 読みやすさ
  • 書きやすさ
  • テストの実行速度
  • committee-railsを使うことでOpenAPIの整合性の維持 & APIレスポンスに対してのテストの充実度

テストはコードを書く以上必ず存在するものなので、ストレスなくテストを読めたり、書いたりできることでかなりチームの開発にプラスな影響を与えると思います。この記事を参考にテストの書き方を改善してみようと思った方がいると嬉しいです。ありがとうございました。