TR kizaki Tech Memo

Airbnb-clone by ruby and rails part2

Airbnb-clone video4-6 setting users API

  • icon
<div class="relative>
  <div class="absolute inset-0 flex item-center" aria-hidden="true">
    <div class="w-full border-gray-300"></div>
  </div>
  <div class="relative flex justify-center">
    <span class="px-2 bg-white text-sm text-gray-500">Continue</span>
  </div>
</div> 
<div class="border border-gray-300 rounded-md px-3 py-2 shadow-sm focus-within:ring-1 focus-within:ring-indigo-600 focus-within:border-indigo-600">
 <label for="name" class="block text-xs font-medium text-gray-900">Name</label>
 <input type="text" name="name" id="name" class="block w-full border-0 p-0 text-gray-900 placeholder-gray-500 focus:ring-0 sm:text-sm" placeholder="Jan
</div>

Input Group

<!--
  This example requires some changes to your config:
  

// tailwind.config.js module.exports = { // ... plugins: [ // ... require('@tailwindcss/forms'), ], }

-->
<div>
<label for="price" class="block text-sm font-medium leading-6 text-gray-900">Price</label>
<div class="relative mt-2 rounded-md shadow-sm">
  <div class="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3">
    <span class="text-gray-500 sm:text-sm">$</span>
  </div>
  <input type="text" name="price" id="price" class="block w-full rounded-md border-0 py-1.5 pl-7 pr-20 text-gray-900 ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6" placeholder="0.00">
  <div class="absolute inset-y-0 right-0 flex items-center">
    <label for="currency" class="sr-only">Currency</label>
    <select id="currency" name="currency" class="h-full rounded-md border-0 bg-transparent py-0 pl-2 pr-7 text-gray-500 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm">
      <option>USD</option>
      <option>CAD</option>
      <option>EUR</option>
    </select>
  </div>
</div>
</div>

https://www.w3.org/TR/SVG/

https://heroicons.com/

6. Setting up the Users API

https://www.youtube.com/watch?v=8v202u1i1dg&list=PLCawOXF4xaJK1_-KVgXyREULRVy_W_1pe&index=6

  • api directoryとuser controllerを作成
┌─(~/dev/monchifc)──────────────┐
└─(18:49:54 on feature-modal)──> mkdir app/controllers/api      ──(Wed,Aug02)─┘
┌─(~/dev/monchifc)──────────────┐
└─(10:47:19 on feature-modal)──> touch app/controllers/api/users_controller.rb
config/routes.rb

Rails.application.routes.draw do
  devise_for :users

  
  root "home#index"

  namespace :api do
    resources :users, only: :show
  end
end
app/controllers/api/users_controller.rb

module Api
    class UsersController < ApplicationController 
        def show
        end
    end
end
  • user_specでapiの動きをdefine
  • RSpecでは、テストコードのことをスペック(仕様)と言う
┌─(~/dev/monchifc)──────────────┐
└─(11:29:09 on feature-modal ✭)──> mkdir spec/requests/api      ──(Thu,Aug03)─┘
┌─(~/dev/monchifc)──────────────┐
└─(11:29:41 on feature-modal ✭)──> touch spec/requests/api/user_spec.rb
spec/requests/api/user_spec.rb

require 'rails_helper'

RSpec.describe "Api::Users", type: :request do
  describe "GET show" do
    context "user exists" do
        it "is successful" do
        end
    end

    context "user does not exist" do
        it "is not found" do
        end
    end
  end
end
  • API用のcontroller routeを作成
bundle exec rails routes -c api/users 
└─(12:04:50 on feature-modal ✹ ✭)──> bundle exec rails routes -c api/users
  Prefix Verb URI Pattern              Controller#Action
api_user GET  /api/users/:id(.:format) api/users#show

modelを作ろうとしたらerror

└─(13:14:12 on feature-modal ✹ ✭)──> bundle exec rails g model user
      invoke  active_record
The name 'User' is either already used in your application or reserved by Ruby on Rails. Please choose an alternative or use --skip-collision-check or --force to skip this check and run this generator again.
  • deviseで認証するためのモデルを作成

deviseとはrailsで作ったwebアプリケーションに簡単に認証機能を実装できるgem

┌─(~/dev/monchifc)──────────────┐
└─(09:51:25 on feature-api ✹)──> bundle exec rails g devise user
      invoke  active_record
      create    db/migrate/20230806055559_add_devise_to_users.rb
File unchanged! Either the supplied flag value not found or the content has already been inserted!    app/models/user.rb
       route  devise_for :users
  • DBはremoveする
└─(09:55:59 on feature-api ✹ ✭)──> rm db/migrate/20230806055559_add_devise_to_users.rb
  • factory_bot_railsをsettingする
  • RSpecのテストコードを効率化できるツール「FactoryBot」

spec/factories のdirectoryを作成。その下にemailやpasswordなどのuser sample dataをdescribeする

実際のテストコードではFactoryで定義したデータの内、テストしたい部分だけを上書きしていく

テストのたびにテストデータを用意する必要がないのでとても便利

┌─(~/dev/monchifc)──────────────┐
└─(14:35:43 on feature-modal ✹ ✭)──> mkdir spec/factories       ──(Thu,Aug03)─┘
┌─(~/dev/monchifc)──────────────┐
└─(14:36:48 on feature-modal ✹ ✭)──> touch spec/factories/users.rb

https://github.com/thoughtbot/factory_bot_rails

  • user_specのFactoryBotという文言を省略するための構造をhelper file上につくる
spec/rails_helper.rb

RSpec.configure do |config|
  config.include FactoryBot::Syntax::Methods
  • この雛形にsample dataを記載していく https://qiita.com/mmaumtjgj/items/39144c2e4314a798a0e6
spec/factories/users.rb

FactoryBot.define do
 factory :user do
    sequence(:email) { |i| "foo_#{i}@example.com" }
 #↑これが呼び出されるたびに、iの部分に数字が一つずつ増えて入るため、一意性が保たれる
    password { "password" }
  end
end
  • matcher sample

matcherは「期待値と実際の値を比較して、一致した(もしくは一致しなかった)という結果を返すオブジェクト」のこと。

・空であることを望んでいる
expect(user).to be_empty
# user.empty? が true になればパスする

・存在することを望んでいる
expect(user).to be_valid 
# user.valid? が true になればパスする

・存在しないことを望んでいる/not_to(to_not)を使わない記入法
expect(user).to be_invalid
# user.invalid? が true になればパスする
spec/requests/api/users_spec.rb

require 'rails_helper'

RSpec.describe "Api::Users", type: :request do
  describe "GET show" do
    context "user exists" do
        it "is successful" do
            user = create(:user)
            get api_user_path(user), headers:{ 'ACCEPT' => 'application/json' }
            expect(response).to be_successful
        end
    end

    context "user does not exist" do
        it "is not found" do
        end
    end
  end
end
  • Test
└─(10:25:38 on feature-api ✖ ✹ ✭)──> bundle exec rspec spec/requests/api/users_spec.rb
//sample 

user = FactoryBot.build(:user, nickname: "")
↓
user = build(:user, nickname: "")
  • set rspec default get request format to json

https://stackoverflow.com/questions/11022839/set-rspec-default-get-request-format-to-json

let(:headers) do
    {'ACCEPT' => 'application/json'}

headers: headers の追加が特徴的。

spec/requests/api/users_spec.rb

require 'rails_helper'

RSpec.describe "Api::Users", type: :request do
  describe "GET show" do
    let(:headers) do
        {'ACCEPT' => 'application/json'}
    end

    context "user exists" do
        it "is successful" do
            user = create(:user)
            get api_user_path(user), headers: headers
            expect(response).to be_successful
        end
    end

    context "user does not exist" do
        it "is not found" do
        end
    end
  end
end
  • Test
└─(10:30:25 on feature-api ✖ ✹ ✭)──> bundle exec rspec spec/requests/api/users_spec.rb
app/controllers/api/users_controller.rb

module Api
    class UsersController < ApplicationController 
        def show
         user = User.find(params[:id])

         respond_to do |format| 
            format.json do
                render json: user.to_json, status: ok
         end
      end
    end
end
└─(10:30:25 on feature-api ✖ ✹ ✭)──> bundle exec rspec spec/requests/api/users_spec.rb
spec/requests/api/user_spec.rb

require 'rails_helper'

RSpec.describe "Api::Users", type: :request do
  describe "GET show" do
    let(:headers) do
        {'ACCEPT' => 'application/json'}
    end

    context "user exists" do
        it "is successful" do
            user = create(:user)
            get api_user_path(user), headers: headers
            expect(response).to be_successful
        end
    end

    context "user does not exist" do
        it "is not found" do
          get api_user_path, id: "junk", headers: headers 
          expect(response).to be_not_found
        end
    end
  end
end

https://stackoverflow.com/questions/17890419/when-to-display-record-not-found-page-or-return-http-404

require 'rails_helper'

RSpec.describe "Api::Users", type: :request do
  describe "GET show" do
    let(:headers) do
        {'ACCEPT' => 'application/json'}
    end

    context "user exists" do
        it "is successful" do
            user = create(:user)
            get api_user_path(user), headers: headers
            expect(response).to be_successful
        end
    end

    context "user does not exist" do
        it "is not found" do
          get api_user_path(id: "junk"), params: { id: "junk" }, headers: headers 
          expect(response).to eq 404
        end
    end
  end
end
require 'rails_helper'

RSpec.describe "Api::Users", type: :request do
  describe "GET show" do
    let(:headers) do
        {"ACCEPT" => "application/json"}
    end

    context "user exists" do
        it "is successful" do
            user = create(:user)
            get api_user_path(user), headers: headers
            expect(response).to be_successful
        end
    end

    context "user does not exist" do
        it "is not found" do
          get api_user_path(id: "junk"), headers: headers 
          expect(response).to eq 404
        end
     end
   end
end
  • Test
└─(10:30:25 on feature-api ✖ ✹ ✭)──> bundle exec rspec spec/requests/api/users_spec.rb

Failure/Error: user = User.find(params[:id])

idのjunkは存在しないので Couldn't find User with id = junk

  • rescueメソッドを追加

    app/controllers/api/users_controller.rb
    
    module Api
     class UsersController < ApplicationController 
         def show
           user = User.find(params[:id])
    
         respond_to do |format| 
            format.json do
                render json: user.to_json, status: :ok
              end
            end
        rescue ActiveRecord::RecordNotFound => e
          binding.pry
    
          respond_to do |format|
            format.json do
                render json: { error: e.message }.to_json, status: 404
              end
            end
          end
      end
    end
    

*** stringであるok に :ok の形でdefineしないとerrorになるので注意

└─(10:57:20 on feature-api ✖ ✹ ✭)──> bundle exec rspec spec/requests/api/users_spec.rb

F.

Failures:

  1) Api::Users GET show user exists is successful
     Failure/Error: render json: user.to_json, status: ok
     
     NameError:
       undefined local variable or method `ok' for #<Api::UsersController:0x0000000000cf30>
  • Errorがなぜ起こるのか確認のためpry デバックツール 追加
  • binding.pryはコード上にbinding.pryを記述することでbinding.pryの書かれている箇所までの処理を実行し、

binding.pryの書かれている箇所で処理を一時的に止めることができる。

https://github.com/pry/pry-rails

└─(17:06:26 on feature-modal ✹ ✭)──> bundle                 1 ↵ ──(Thu,Aug03)─┘
Fetching gem metadata from https://rubygems.org/..........
Resolving dependencies...
Fetching coderay 1.1.3
Installing coderay 1.1.3
Fetching pry 0.14.2
Installing pry 0.14.2
Fetching pry-rails 0.3.9
Installing pry-rails 0.3.9
Bundle complete! 21 Gemfile dependencies, 93 gems now installed.
Bundled gems are installed into `./.bundle`
  • rspecを実行するとpryのdebug consoleが立ち上がる

rescueをdebugしてErrorの原因確認

└─(10:45:13 on feature-api ✖ ✹ ✭)──>  bundle exec rspec spec/requests/api/users_spec.rb

F
From: /Users/$HOME/dev/$PROJECTNAME/app/controllers/api/users_controller.rb:12 Api::UsersController#show:

     3:  def show
     4:    user = User.find(params[:id])
     5:    
     6:  respond_to do |format| 
     7:     format.json do
     8:         render json: user.to_json, status: :ok
     9:       end
    10:     end
    11: rescue ActiveRecord::RecordNotFound => e
 => 12:   binding.pry
    13:    
    14:   respond_to do |format|
    15:     format.json do
    16:         render json: { error: e.message }.to_json, status: 404
    17:       end
    18:     end
    19:   end

[1] pry(#<Api::UsersController>)> e
=> #<ActiveRecord::RecordNotFound: Couldn't find User with 'id'=junk>
[2] pry(#<Api::UsersController>)> e.message
=> "Couldn't find User with 'id'=junk"
[3] pry(#<Api::UsersController>)> exit
.

debugしてもちゃんと動作している

  • add status argument
require 'rails_helper'

RSpec.describe "Api::Users", type: :request do
  describe "GET show" do
    let(:headers) do
        {"ACCEPT" => "application/json"}
    end

    context "user exists" do
        it "is successful" do
            user = create(:user)
            get api_user_path(user), headers: headers
            expect(response).to be_successful
        end
    end

    context "user does not exist" do
        it "is not found" do
          get api_user_path(id: "junk"), headers: headers 
          expect(response.status).to eq 404
        end
     end
   end
end
  • Test success!!
└─(11:03:35 on feature-api ✖ ✹ ✭)──> bundle exec rspec spec/requests/api/users_spec.rb

..

Finished in 0.14728 seconds (files took 1.45 seconds to load)
2 examples, 0 failures

Got some error in Rails on sprockets precompilation step after tailwind build file generated

This is to prevent error caused by the sassc-rails gem when running-test

//config/environments/test.rb
#add
config.assets.css_compressor = nil
  • solved