大ちゃんの駆け出し技術ブログ

RUNTEQ受講生のかわいいといわれるアウトプットブログ

【リクエストスペック②】リクエストスペックの作成

はじめに

こんにちは!大ちゃんの駆け出し技術ブログです。

1日空けてしまいましたが、本日はリクエストスペックの記述方法についてまとめていきたいと思います。

先日出した記事ではAPIの概要などを簡単に紹介しました。

sakitadaiki.hatenablog.com

本記事ではいよいよリクエストスペックを書いていきます!

テスト内容は前回の記事の中で紹介した『ログインユーザーの情報にアクセスできること』です。正しいパラメータを入力した上で、POST形式でlocalhost:3000/api/v1/authenticationにアクセスすると下記JSON形式の情報が取得できことを検証します。

{
    "data": {
        "id": "1",
        "type": "user",
        "attributes": {
            "name": "MyString1",
            "email": "MyText1"
        }
    }
}

ファイルの作成

早速スペックファイルを作成します。

他のシステムスペックやモデルスペックでもファイル作成用のコマンドがありますが、リクエストスペックにもあります。

$ bin/rails g rspec:request ファイル名

今回はapi/v1/配下のファイルをテストするため、テストファイルも同様にapi/v1/配下のフォルダに格納した方が良さそうです。どうするかというとファイル名の手前にApi::V1::を置くだけです。そして今回テストするファイルはauthenticationですので、ファイル名もそれに合わせて指定しましょう。

$ bin/rails g rspec:request Api::V1::authentication

こちらを実行すると指定したディレクトリにフォルダが作成されました。

$ bin/rails g rspec:request Api::V1::authentication
Running via Spring preloader in process 6744
      create  spec/requests/api/v1/authentications_spec.rb

無事ファイルが作成されました。

ファイルの中身を説明

作成されたフォルダの中身を見てみましょう。

# spec/requests/api/v1/authentications_spec.rb
require 'rails_helper'

RSpec.describe "Api::V1::Authentications", type: :request do
  describe "GET /api/v1/authentications" do
    it "works! (now write some real specs)" do
      get api_v1_authentications_path
      expect(response).to have_http_status(200)
    end
  end
end

いつものシステムスペックやモデルスペック作成時とはかなり見た目が違うのではないでしょうか。

まず、type: :requestの部分からわかる通り、テストの種類がリクエストであることがわかると思います。

その下のdescribeの中身は現状「GET形式で/api/v1/authenticationsにアクセスする」という記述になっていますね。これはリクエストスペックの作成時はデフォルトでGET形式が設定されているからです。今回テストするのはPOST形式ですので後ほどこちらは修正します。

その下のitの中身も目新しいかなと思います。

get api_v1_authentications_path
expect(response).to have_http_status(200)

get api_v1_authentications_pathについては直感的に理解可能かと思います。describe部分で記述している通り、「GET形式で/api/v1/authenticationsにアクセス」を行っています。こちらはHTTPリクエストの形式です。これを例えばPOST形式にする場合、post api_v1_authentications_pathを使うことができます。

次の検証部分ですが、これはHTTPステータスコードの検証をしています。HTTPステータスコードを知らないという方はMDNの説明をご覧ください。

HTTP レスポンスステータスコードは、特定の HTTP リクエストが正常に完了したどうかを示します。レスポンスは 5 つのクラスに分類されています。 1. 情報レスポンス (100199), 2. 成功レスポンス (200299), 3. リダイレクト (300399), 4. クライアントエラー (400499), 5. サーバエラー (500599)

developer.mozilla.org

have_http_status(200)とすることでステータスコードが200、つまりHTTPリクエストが正常に完了したことを検証しています。反対に200を400などにすることでクライアントエラーが発生するかどうかを検証することができます。

リクエストスペックについて少し理解ができたのではないでしょうか?

POST形式でのアクセス方法

それではPOST形式でlocalhost:3000/api/v1/authenticationに正しいパラメータでアクセスすると、JSON形式の情報が取得できことを検証するテストを書いていきます。テストのマッチャは記載せず、describecontextitのみを記載して検証ファイルの流れを作ります。

require 'rails_helper'

RSpec.describe "Api::V1::Authentications", type: :request do
  describe "POST /api/v1/authentication" do
    context '正しいパラメータが入力された時' do
      it 'JSON形式の情報を返す' do
      end
    end
  end
end

それでは正しいパラメータを入力する方法から紹介していきます。

UserのFactoryBotは現在以下のように定義されています。

FactoryBot.define do
  factory :user do
    sequence(:name)  { |n| "MyString#{n}" }
    sequence(:email) { |n| "MyText#{n}" }
    password { 'password' }
  end
end

このFactoryBotのユーザーでログインしたとき、ログインユーザーの情報を取得できるかどうかを検証します。リクエストスペックファイル内ではlet!を使用してユーザーを新規作成します。

require 'rails_helper'

RSpec.describe "Api::V1::Authentications", type: :request do
  describe "POST /api/v1/authentications" do
    let!(:user) { create(:user) }
    context '正しいパラメータが入力された時' do
      it 'JSON形式の情報を返す' do
      end
    end
  end
end

次にPOST形式でlocalhost:3000/api/v1/authenticationにアクセス方法に移ります。これは先に記述を見せてから説明します。

require 'rails_helper'

RSpec.describe "Api::V1::Authentications", type: :request do
  describe "POST /api/v1/authentications" do
    let!(:user) { create(:user) }
    let(:password) { 'password' }
    context '正しいパラメータが入力された時' do
      it 'JSON形式の情報を返す' do
        post api_v1_authentication_path, headers: { CONTENT_TYPE: 'application/json', ACCEPT: 'application/json' }
      end
    end
  end
end

post api_v1_authentication_pathならわかると思います。POST形式でlocalhost:3000/api/v1/authenticationにアクセスしています。問題は後半部分のheaders以降になるかと思います。

headers: { CONTENT_TYPE: 'application/json', ACCEPT: 'application/json' }

これを簡単に説明している記事を見つけましたので共有します。

qiita.com

ACCEPTとは「クライアント側がどんなデータを処理できるか」を表しています。つまり受け取るファイル形式であるJSONを許容(ACCEPT)しているということです。受け取るファイルはapplication/jsonを指定しています。そもそもこの記述がないとテストでJSON形式のファイルを取得できないという意味にも取れます。

CONTENT_TYPEとは「実際にどんな形式のデータを送信したか」を表しています。サーバー側からクライアント側へ渡すファイル形式がJSONであるということです。こちらもapplication/jsonを指定しているため、このJSONファイルを送信することを指定しています。

つまり、ACCEPTとCONTENT_TYPEに同じファイル(application/json)指定することで、JSONファイルの送受信を可能にしているということです。JSONデータを取得するリクエストスペックでは必須の記述になると思います。

次に、正しいパラメータを入力する方法です。下記画像のQuery Paramsの入力部分になります。

https://i.gyazo.com/ecf0946222a64cc5d186bc6590f59ac2.png

これはparamsオプションを使って指定することができます。

RSpec.describe "Api::V1::Authentications", type: :request do
  describe "POST /api/v1/authentications" do
    let!(:user) { create(:user) }
    let(:password) { 'password' }
    context '正しいパラメータが入力された時' do
      it 'JSON形式の情報を返す' do
        post api_v1_authentication_path, headers: { CONTENT_TYPE: 'application/json', ACCEPT: 'application/json' }, params: { email: user.email, password: user.password }.to_json
      end
    end
  end
end

パラメータ取得部分

params: { email: user.email, password: password }.to_json

こちらもJSON形式でのリクエストという理由でto_jsonを指定しています。また、passwordのパラメータはuser.passwordでは入力できません。user.passwordは'password'をハッシュ化したものだからです。

crypted_password: "$2a$10$HO18dOf1JpzNaQ2Cq7JwHOdNe/KMEurfmt7e6sYZ.AxkfnFvF7jBa"

したがって、let(:password) { 'password' }で変数passwordをパラメータ部分に指定することでハッシュ化前の正しいパスワードを入力しています。

これでPOST形式でlocalhost:3000/api/v1/authenticationにアクセスできるようになりましたが、postメソッド部分がオプションのせいで少し頭でっかちですので、変数request_hashにまとめてしまいましょう。

require 'rails_helper'

RSpec.describe "Api::V1::Authentications", type: :request do
  describe "POST /api/v1/authentications" do
    let!(:user) { create(:user) }
    let(:password) { 'password' }
    context '正しいパラメータが入力された時' do
      it 'JSON形式の情報を返す' do
        request_hash = { headers: { CONTENT_TYPE: 'application/json', ACCEPT: 'application/json' }, params: { email: user.email, password: password }.to_json }
        post api_v1_authentication_path, request_hash
      end
    end
  end
end

リクエストスペックのマッチャ

それでは実際にマッチャを使って検証してみましょう。

まず、POST形式でのアクセスは成功しているはずですので、HTTPステータスコードは200を返しているハズです。よって上述したhave_http_status(200)を使用してみましょう。

require 'rails_helper'

RSpec.describe "Api::V1::Authentications", type: :request do
  describe "POST /api/v1/authentications" do
    let!(:user) { create(:user) }
    let(:password) { 'password' }
    context '正しいパラメータが入力された時' do
      it 'JSON形式の情報を返す' do
        request_hash = { headers: { CONTENT_TYPE: 'application/json', ACCEPT: 'application/json' }, params: { email: user.email, password: password }.to_json }
        post api_v1_authentication_path, request_hash
        expect(response).to have_http_status(200)
      end
    end
  end
end

テストはパスしたハズです。しかし、これだけだとアクセスは成功していることはわかりますが、期待するJSONファイルを取得できているのかがわかりません。

どのようにしてJSONファイルを取得するのかというと、JSON.parse(response.body)で取得することができます。テストをbinding.pryで止めて確認してみましょう。

JSON.parse(response.body)
=> {"data"=>{"id"=>"1", "type"=>"user", "attributes"=>{"name"=>"MyString1", "email"=>"MyText1"}}}

確かにJSON形式のファイルを取得できました。この中身を検証するためのテストを書きましょう。各キーに対する値が正しければ期待するJSONファイルが取得できているということを検証できます。

require 'rails_helper'

RSpec.describe "Api::V1::Authentications", type: :request do
  describe "POST /api/v1/authentications" do
    let!(:user) { create(:user) }
    let(:password) { 'password' }
    context '正しいパラメータが入力された時' do
      it 'JSON形式の情報を返す' do
        request_hash = { headers: { CONTENT_TYPE: 'application/json', ACCEPT: 'application/json' }, params: { email: user.email, password: password }.to_json }
        post api_v1_authentication_path, request_hash
        expect(JSON.parse(response.body).dig('data', 'id').to_i).to eq(user.id)
        expect(JSON.parse(response.body).dig('data', 'attributes', 'name')).to eq(user.name)
        expect(JSON.parse(response.body).dig('data', 'attributes', 'email')).to eq(user.email)
        expect(response).to have_http_status(200)
      end
    end
  end
end

digメソッドについて簡単に説明します。digメソッドはネストしたハッシュから安全に値を取り出すことができるメソッドです。例えば、expect(JSON.parse(response.body).dig('data', 'id')の部分ですが、dig('data', 'id')とすることで、最初のキーである'data'の値のハッシュのキーである'id'の値である'1'を取り出しています。

{"data"=>{"id"=>"1", "type"=>"user", "attributes"=>{"name"=>"MyString1", "email"=>"MyText1"}}}

ようは、深い階層にあたるハッシュの値を取得するメソッドということになります。下記2つの文もそれぞれ深い階層にある値を取り出し、その値が期待しているものかを検証しているのです。

expect(JSON.parse(response.body).dig('data', 'attributes', 'name')).to eq(user.name)
expect(JSON.parse(response.body).dig('data', 'attributes', 'email')).to eq(user.email)

テストを実行するとパスするかと思います。

終わりに

2つの記事に分けましたがやはりボリュームが大きかったですね。

リクエストスペックは1つ目の記事にも書きましたが、APIのための統合テストです。ですので、基本的にAPIを使用しない場合はメジャーなシステムスペックを使用しましょう!

以上、大ちゃんの駆け出し技術ブログでした!