【Rails】最低限の開発スタートを切るためのRailsセットアップ

## 本記事作成に至った経緯(Background)
Railsを使ってみたいと思っていてもどこから始めればいいの？であったり、現場で使われているRailsってどんな感じなの？ということを聞くことがあるので、現場でよく使われているようなセットアップで環境構築をして最低限ここまでやればRails開発がスタートできますという部分をやっていきます。
:warning: RubyのバージョンやRailsのバージョンを安定板の最新版に合わせてください。今回は`ruby-3.4.3`と`Rails8.0.2`でやっていきます。
 ## 前提
今回はAPIモードで`rails new`します。理由は、昨今のRails周りの構成ではFrontendをNext.jsで作りBackendをRailsで作ることが多いからです。

Dockerを使うのでセットアップは各自でお願いします。また今回はWindowsのwsl上でコマンド実行していきます。
現場ではGithubを使うことが多いと思うのでリポジトリも作られたうえで実施すると良いかなと思います。

それでは本題に移って始めます。

## 本題
### 1. Railsの起動に必要なファイル作成
まず用意するファイルは6つです。
`Gemfile`、`Gemfile.lock`、`Dockerfile`、`Dockerfile.dev`、`entrypoint.sh`、`compose.yaml`


```Gemfile
source 'https://rubygems.org'
gem 'rails', '~> 8.0', '>= 8.0.2'
```
Gemfileを作成。Railsの8系を使っていきます。

```Gemfile.lock

```

空のGemfile.lockを作成。

```Dockerfile
FROM ruby:3.4.3

WORKDIR /docbase_rails
COPY . .
RUN bundle install

RUN apt-get update -qq && \
    apt-get install --no-install-recommends -y \
    build-essential default-libmysqlclient-dev git pkg-config \
    vim curl

COPY entrypoint.sh /usr/bin/
RUN chmod +x /usr/bin/entrypoint.sh
ENTRYPOINT ["entrypoint.sh"]
EXPOSE 3000

CMD ["rails", "server", "-b", "0.0.0.0"]
```
Dockerfileを作成。

```Dockerfile.dev
FROM ruby:3.4.3

WORKDIR /docbase_rails
COPY . .
RUN bundle install

RUN apt-get update -qq && \
    apt-get install --no-install-recommends -y \
    build-essential default-libmysqlclient-dev git pkg-config \
    vim curl

COPY entrypoint.sh /usr/bin/
RUN chmod +x /usr/bin/entrypoint.sh
ENTRYPOINT ["entrypoint.sh"]
EXPOSE 3000

CMD ["rails", "server", "-b", "0.0.0.0"]
```
先ほど作ったDockerfileと内容同じと思ったかもしれません、これは`rails new`コマンドの実行でDockerfileがproduction用に自動で上書きされるため`Dockerfile.dev`も用意しています。
`vim`などをインストールしてますが、不要なら削って大丈夫です

```entrypoint.sh
#!/bin/sh

set -e

rm -f /docbase_rails/tmp/pids/server.pid

exec "$@"
```
entrypoint.shを作成。

```compose.yaml
services:
  db:
    image: mysql:8.0
    volumes:
      - mysql_volume:/var/lib/mysql
    environment:
      LANG: C.UTF-8
      TZ: Asia/Tokyo
      MYSQL_ROOT_PASSWORD: root
    ports:
      - "3306:3306"
  web:
    build:
      context: .
      dockerfile: Dockerfile.dev
    volumes:
      - .:/docbase_rails
    ports:
      - "3000:3000"
    depends_on:
      - db

volumes:
  mysql_volume:
    driver: local
```
compose.yamlを作成。

これで準備は完了です。それではrailsアプリを作っていきましょう。
### 2. rails newの実行
```powershell
$ docker compose run --rm --no-deps web rails new . --force --database=mysql --skip-
test --skip-ci --skip-kamal --skip-thruster --skip-solid --api
[+] Creating 2/1
 ✔ Network docbase_rails_default        Created                                                                 0.1s 
 ✔ Volume "docbase_rails_mysql_volume"  Created                                                                 0.0s 
[+] Building 33.1s (12/12) FINISHED                                                                   docker:default
 => [web internal] load build definition from Dockerfile.dev                                                    0.0s
 => => transferring dockerfile: 414B                                                                            0.0s
 => [web internal] load metadata for docker.io/library/ruby:3.4.3                                               0.0s
 => [web internal] load .dockerignore                                                                           0.0s
 => => transferring context: 2B                                                                                 0.0s
 => [web 1/7] FROM docker.io/library/ruby:3.4.3                                                                 0.0s
 => [web internal] load build context                                                                           0.0s
 => => transferring context: 30.86kB                                                                            0.0s
 => CACHED [web 2/7] WORKDIR /docbase_rails                                                                     0.0s
 => [web 3/7] COPY . .                                                                                          0.1s
 => [web 4/7] RUN bundle install                                                                               22.5s
 => [web 5/7] RUN apt-get update -qq &&     apt-get install --no-install-recommends -y     build-essential def  9.5s 
 => [web 6/7] COPY entrypoint.sh /usr/bin/                                                                      0.1s 
 => [web 7/7] RUN chmod +x /usr/bin/entrypoint.sh                                                               0.3s 
 => [web] exporting to image                                                                                    0.5s 
 => => exporting layers                                                                                         0.5s 
 => => writing image sha256:c0806fda6a38daefdea83c5e2c5da2470af771844a93732e738726027d969986                    0.0s 
 => => naming to docker.io/library/docbase_rails-web                                                            0.0s 
Based on the specified options, the following options will also be activated:

  --skip-javascript [due to --api]
  --skip-hotwire [due to --skip-javascript]
  --skip-asset-pipeline [due to --api]

       exist  
       force  README.md
      create  Rakefile
      create  .ruby-version
      create  config.ru
      create  .gitignore
      create  .gitattributes
       force  Gemfile
         run  git init -b main from "."
warning: re-init: ignored --initial-branch=main
Reinitialized existing Git repository in /docbase_rails/.git/
      create  app
      create  app/assets/stylesheets/application.css
      create  app/controllers/application_controller.rb
      create  app/helpers/application_helper.rb
      create  app/jobs/application_job.rb
      create  app/mailers/application_mailer.rb
      create  app/models/application_record.rb
      create  app/views/layouts/application.html.erb
      create  app/views/layouts/mailer.html.erb
      create  app/views/layouts/mailer.text.erb
      create  app/views/pwa/manifest.json.erb
      create  app/views/pwa/service-worker.js
      create  app/assets/images
      create  app/assets/images/.keep
      create  app/controllers/concerns/.keep
      create  app/models/concerns/.keep
      create  bin
      create  bin/brakeman
      create  bin/dev
      create  bin/rails
      create  bin/rake
      create  bin/rubocop
      create  bin/setup
      force  Dockerfile
      create  .dockerignore
      create  bin/docker-entrypoint
      create  .rubocop.yml
      create  config
      create  config/routes.rb
      create  config/application.rb
      create  config/environment.rb
      create  config/cable.yml
      create  config/puma.rb
      create  config/storage.yml
      create  config/environments
      create  config/environments/development.rb
      create  config/environments/production.rb
      create  config/environments/test.rb
      create  config/initializers
      create  config/initializers/assets.rb
      create  config/initializers/content_security_policy.rb
      create  config/initializers/cors.rb
      create  config/initializers/filter_parameter_logging.rb
      create  config/initializers/inflections.rb
      create  config/initializers/new_framework_defaults_8_0.rb
       create  config/locales
      create  config/locales/en.yml
      create  config/master.key
      append  .gitignore
      create  config/boot.rb
      create  config/database.yml
      create  db
      create  db/seeds.rb
      create  lib
      create  lib/tasks
      create  lib/tasks/.keep
      create  log
      create  log/.keep
      create  public
      create  public/400.html
      create  public/404.html
      create  public/406-unsupported-browser.html
      create  public/422.html
      create  public/500.html
      create  public/icon.png
      create  public/icon.svg
      create  public/robots.txt
      create  script
      create  script/.keep
      create  tmp
      create  tmp/.keep
      create  tmp/pids
      create  tmp/pids/.keep
      create  vendor
      create  vendor/.keep
      create  storage
      create  storage/.keep
      create  tmp/storage
      create  tmp/storage/.keep
      remove  app/assets
      remove  app/helpers
      remove  test/helpers
      remove  app/views/layouts/application.html.erb
      remove  app/views/pwa
      remove  public/400.html
      remove  public/404.html
      remove  public/406-unsupported-browser.html
      remove  public/422.html
      remove  public/500.html
      remove  public/icon.png
      remove  public/icon.svg
      remove  config/initializers/assets.rb
      remove  app/assets/stylesheets/application.css
      remove  config/initializers/content_security_policy.rb
      remove  config/initializers/new_framework_defaults_8_0.rb
         run  bundle install --quiet
         run  bundle lock --add-platform=x86_64-linux
Writing lockfile to /docbase_rails/Gemfile.lock
         run  bundle binstubs bundler
```
ファイルが生成されました。rails8の詳細なコマンドは公式を確認してください。
https://railsguides.jp/v8.0/command_line.html
skipオプションをもっと付与してもいいと思いますが、一旦このくらいでやります。
またrails7からrails8の変更点としてsolid cacheなど多くの機能が追加されましたが今回はスキップしました。各自使いたい機能を考えて`rails new`のオプションを実行して頂ければと思います。

### 3. localhost:3000にアクセスするまで
railsファイルが生成できたので続いて起動まで進めていきます。
### DB接続情報の変更
```config/database.yml
default: &default
  adapter: mysql2
  encoding: utf8mb4
  pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
  username: root
  password: root
  host: db

development:
  <<: *default
  database: docbase_rails_development

test:
  <<: *default
  database: docbase_rails_test

production:
  <<: *default
  database: docbase_rails_production
  username: docbase_rails
  password: <%= ENV["DOCBASE_RAILS_DATABASE_PASSWORD"] %>
```
ハードコーディングしてますが後々環境変数にしましょう。

### 停止コマンド
```powershell
$ docker compose -f compose.yaml down
```
### 初回buildコマンド
```powershell
$ docker compose -f compose.yaml build
```
### 起動コマンド
```powershell
$ docker compose -f compose.yaml up
```
### DB周りのコマンド

![スクリーンショット 2025-06-11 172438.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3286459/fddbc070-c565-499b-92cd-69fc6638f1e7.png)
起動後に http://localhost:3000 にアクセスして画像のようにNoDatabaseErrorになった場合は`db:create`コマンドを実施しましょう。
```powershell
$ docker compose -f compose.yaml run --rm web rake db:create
[+] Creating 1/0
 ✔ Container docbase_rails-db-1  Running                                                                                                                                                                  0.0s 
Created database 'docbase_rails_development'
Created database 'docbase_rails_test'
```

![スクリーンショット 2025-06-11 172720.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3286459/20d5cd1d-770f-4426-bb64-957edfc7192e.png)

DBが用意されて http://localhost:3000 の画面をリロードすると上記のような画面に切り替わっていると思います。これでセットアップ完成です。

【補足】
```powershell
$ docker compose -f compose.yaml run --rm web rake db:migrate:reset
[+] Creating 1/0
 ✔ Container docbase_rails-db-1  Running                                                                                                                                                                  0.0s 
Dropped database 'docbase_rails_development'
Dropped database 'docbase_rails_test'
Created database 'docbase_rails_development'
Created database 'docbase_rails_test'
```
`db:create`の他にも`db:migrate`や`db:migrate:reset`や`db:drop`などコマンドは多くあるので、適宜使ってみてください。

## rspecのGem導入
railsアプリでよく使われるGemが本当に多くありますが、今回は最低限としてrspecの準備をしていきます。これを選んだ理由は、実際の現場ではテストを書くことが多いからです。
### ⅰ. gem追加
```Gemfile
group :development, :test do
  gem 'rspec-rails'
  gem 'factory_bot_rails'
  gem 'faker'
end
```
`rspec-rails`と一緒に良く使われるGemです。一緒にインストールしてください。
### ⅱ. Gemfileをインストール
```powershell
$ docker compose -f compose.yaml build 
#or
$ docker compose -f compose.yaml run --rm web bundle install
Bundle complete! 25 Gemfile dependencies, 123 gems now installed.
Use `bundle info [gemname]` to see where a bundled gem is installed.
```
Dockerfileの記述があるので、私はGemfile変更する時はbuildし直しています。
### ⅲ. RSpecインストール
```powershell
$ docker compose -f compose.yaml run --rm web rails generate rspec:install
[+] Creating 1/0
 ✔ Container rails_research-db-1  Running                                                  0.0s 
      create  .rspec
      create  spec
      create  spec/spec_helper.rb
      create  spec/rails_helper.rb
```
rspecで利用するファイル群が生成されました。ここからrspecが利用可能になります。
この後サンプルでAPIを作ってみるところセクションでrspecを使ってみます。

## サンプルでAPIを作ってみる
では実際にrailsを使ってAPIを作ってみましょう。本当に簡単なCRUDをやっていきます。
```powershell
$ docker compose -f compose.yaml run --rm web rails g model Prefecture
[+] Creating 1/0
 ✔ Container docbase_rails-db-1  Running                                                                                                                                                         0.0s 
      invoke  active_record
      create    db/migrate/20250611095055_create_prefectures.rb
      create    app/models/prefecture.rb
      invoke    rspec
      create      spec/models/prefecture_spec.rb
      invoke      factory_bot
      create        spec/factories/prefectures.rb
```
Prefectureテーブルを作成しました。
```db/migrate/20250611095055_create_prefectures.rb
class CreatePrefectures < ActiveRecord::Migration[8.0]
  def change
    create_table :prefectures do |t|
      t.timestamps
    end
  end
end
```
db/migrate配下にファイルができていますね。。
これを以下に修正します。
```db/migrate/20250611095055_create_prefectures.rb
class CreatePrefectures < ActiveRecord::Migration[8.0]
  def change
    create_table :prefectures, comment: '都道府県マスタ' do |t|
      t.string :prefecture_cd, null: false, limit: 2, comment: '都道府県コード'
      t.string :name, null: false, comment: '都道府県名'
      t.timestamps
    end
  end
end
```
変更しました。migrationファイルの書き方は以下で確認してください。
[Active Record マイグレーション](https://railsguides.jp/active_record_migrations.html)
```powershell
$ docker compose -f compose.yaml run --rm web rake db:migrate
[+] Creating 1/0
 ✔ Container docbase_rails-db-1  Running                                                                                                                                                         0.0s 
== 20250611095055 CreatePrefectures: migrating ================================
-- create_table(:prefectures, {comment: "都道府県マスタ"})
   -> 0.0549s
== 20250611095055 CreatePrefectures: migrated (0.0550s) =======================
```
マイグレーションが完了しました。

次にモデルファイルに制約を書いておきましょう。
今回は`name`が必須で`prefecture_cd`も必須かつ最大2文字までというDB上の制約を加えてます。

```app/models/prefecture.rb
class Prefecture < ApplicationRecord
  validates :name, presence: true
  validates :prefecture_cd, presence: true, length: { maximum: 2 }
end
```
上記によってアプリケーションのモデルにバリデーション制約を付与できました。
実際の現場だと、`prefecture_cd`は半角数字のみの2文字という制約を加えるかもですね。
（そもそも都道府県は固定で決まっているのでマスタデータとしてseedで投入になることが多く、こういったファイルは作らないと思いますが）
バリデーション制約に関しては、公式の[バリデーションの使い方](https://railsguides.jp/active_record_validations.html#%E3%83%90%E3%83%AA%E3%83%87%E3%83%BC%E3%82%B7%E3%83%A7%E3%83%B3%E3%81%AE%E4%BD%BF%E3%81%84%E6%96%B9)を確認しましょう。

次にroutesファイルを編集します。
```config/routes.rb
Rails.application.routes.draw do
  # Define your application routes per the DSL in https://guides.rubyonrails.org/routing.html

  # Reveal health status on /up that returns 200 if the app boots with no exceptions, otherwise 500.
  # Can be used by load balancers and uptime monitors to verify that the app is live.
  get "up" => "rails/health#show", as: :rails_health_check

  # Defines the root path route ("/")
  # root "posts#index"
  namespace 'api' do
    namespace 'v1' do
      resources :prefectures
    end
  end
end
```
RailsをAPIモードで実装していく場合、namespaceでapi/v1と切ることが多いので今回はそれに合わせてサンプル作っていきます。
```powershell
^CGracefully stopping... (press Ctrl+C again to force)
[+] Stopping 2/2
 ✔ Container docbase_rails-web-1  Stopped                                                                                                                                                        0.2s 
 ✔ Container docbase_rails-db-1   Stopped                                                                                                                                                        0.9s 
canceled
$ docker compose -f compose.yaml up
```
再起動します。
```powershell
$ docker compose -f compose.yaml run --rm web rails routes
[+] Creating 1/0
 ✔ Container docbase_rails-db-1  Running                                                                                                                                                         0.0s 
                                  Prefix Verb   URI Pattern                                                                                       Controller#Action
                      rails_health_check GET    /up(.:format)                                                                                     rails/health#show
                      api_v1_prefectures GET    /api/v1/prefectures(.:format)                                                                     api/v1/prefectures#index
                                         POST   /api/v1/prefectures(.:format)                                                                     api/v1/prefectures#create
                       api_v1_prefecture GET    /api/v1/prefectures/:id(.:format)                                                                 api/v1/prefectures#show
                                         PATCH  /api/v1/prefectures/:id(.:format)                                                                 api/v1/prefectures#update
                                         PUT    /api/v1/prefectures/:id(.:format)                                                                 api/v1/prefectures#update
                                         DELETE /api/v1/prefectures/:id(.:format)                                                                 api/v1/prefectures#destroy
```
`rails routes`でエンドポイントがあることを確認しました。
次にコントローラーの実装です。
```app/controllers/api/v1/prefectures_controller.rb
module Api
  module V1
    class PrefecturesController < ApplicationController
      # 参考: before_actionの使い方とオプションについて(https://pikawaka.com/rails/before_action)
      before_action :set_prefecture, only: [ :show, :update, :destroy ]

      def index
        prefectures = Prefecture.order(created_at: :desc)
        render json: { status: "SUCCESS", message: "Loaded prefectures", data: prefectures }
      end

      def show
        render json: { status: "SUCCESS", message: "Loaded the prefecture", data: @prefecture }
      end

      def create
        prefecture = Prefecture.new(prefecture_params)
        if prefecture.save
          render json: { status: "SUCCESS", data: prefecture }
        else
          render json: { status: "ERROR", data: prefecture.errors }
        end
      end

      def update
        if @prefecture.update(prefecture_params)
          render json: { status: "SUCCESS", message: "Updated the prefecture", data: @prefecture }
        else
          render json: { status: "SUCCESS", message: "Not updated", data: @prefecture.errors }
        end
      end

      def destroy
        @prefecture.destroy
        render json: { status: "SUCCESS", message: "Deleted the prefecture", data: @prefecture }
      end

      private

      def set_prefecture
        @prefecture = Prefecture.find_by(id: params[:id])
        return render json: { status: "ERROR", message: "prefecture not found" } unless @prefecture
      end

      def prefecture_params
        params.require(:prefecture).permit(:prefecture_cd, :name)
      end
    end
  end
end

```
Railsのscaffoldで生成するとnewやeditメソッドもできると思います。今回は間引きます。
ファイル上部のmoduleやclassの書き方は最初はもしかしたら分からないかもしれませんが、routesファイルに記載したnamespaceの単位でapi、v1をmoduleとして書いて、resourcesのprefecturesをPredecturesControllerのように書くようなものだと基本理解して頂けると実務でスムーズに対応できるかなと思います。

さて動作確認です。今回はPostmanを使い実装が上手くできているか確認します。
:warning: Postmanのダウンロードはこちらから https://www.postman.com/


postmanを開きます。私の方でピンクの線を引きました。この＋を押してください。
![スクリーンショット 2025-06-11 203320.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3286459/827b40bc-afa8-4c0c-971e-75adca87c811.png)

`http://localhost:3000/api/v1/prefectures` を入れてSendボタンを押すと下にSUCCESSとかLoaded prefecturesが表示されていれば成功です。上手く行ってない場合は実装でタイプミスしていたり、`docker compose -f compose.yaml up`されてない可能性があるかなと思いますので、確認してください。

![スクリーンショット 2025-06-11 203901.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3286459/d55d916e-02b5-4144-b79a-8ad8a800d054.png)
最初はデータがないです。せっかくcreateメソッドを書いてあるのでデータを作ってみましょう。`http://localhost:3000/api/v1/prefectures` の横の`GET`を`POST`に変更します。またBodyをクリックし、rawを選択し以下をコピペしてください。
```json
{
  "name": "東京都",
  "prefecture_cd": "13"
}
```
入力出来たら、Sendボタンを押し下にSUCCESSが表示されていれば成功です。
![スクリーンショット 2025-06-11 204805.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3286459/55f8ea5e-796f-4931-a102-368ca1d7d6b6.png)

URLは`rails routes`で表示した内容の`POST /api/v1/prefectures(.:format) api/v1/prefectures#create` に対応していることが分かるかと思います。またrawのjsonの記述はcontrollerのparamsで書いたカラムになっています。
```app/controllers/api/v1/prefectures_controller.rb
def prefecture_params
  params.require(:prefecture).permit(:prefecture_cd, :name)
end
```
なので、ここで`name_kana`など存在しないカラムなどを指定して作成しようとすると画像のようにエラーになります。
![スクリーンショット 2025-06-11 205453.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3286459/c25b5540-74d6-4bc8-8fa9-d6eb5bc0ae2d.png)


ここで先ほど一覧画面のGetをしましたが、データが存在するか確認してみましょう。

![スクリーンショット 2025-06-11 205642.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3286459/68088ecd-ec90-4034-8e0f-3002d407dc63.png)

`http://localhost:3000/api/v1/prefectures` を入れてSendボタンを押すと下にSUCCESSとかLoaded prefecturesが表示されているのは最初と同じです。ただ`data`の中に先ほど作ったデータが含まれていますね。ついでに詳細を見てみましょう。

`http://localhost:3000/api/v1/prefectures/1` の1はdataのidを指定しています。ここを存在するデータにすることでそのデータの詳細データを取得することができます。今回はあまり一覧の表示要素と違いがないかもしれませんが、ここは現場だと必要なカラムデータのみを返す処理を加えるのが一般的かなと思います。

![スクリーンショット 2025-06-11 205701.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3286459/479c98d0-7be4-466b-bd45-bbde0f20e5da.png)

またデータは1件しか作りませんでしたので`http://localhost:3000/api/v1/prefectures/2` は存在しないはずですね。試してみましょう。

![スクリーンショット 2025-06-11 210351.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3286459/a3989a9e-3d53-41c9-96e0-e160216bcfbc.png)

`http://localhost:3000/api/v1/prefectures/1` の結果とは異なりデータが見つかりませんでしたね。他にもdeleteメソッドなど任意に試してみてください。


以上でサンプルを終わります。

### rspecの作成
実装ができましたが、ここで終わりではありません。
業務ではテストを書くことが通常のルールかなと思っております。一部書いてみましょう。

まずはGemfileにFakerを入れたのでそれを試してみましょう。
```spec/factories/prefectures.rb
FactoryBot.define do
  factory :prefecture do
    name { Faker::Book.title }
    prefecture_cd { "13" }
  end
end

```
`name`の横の`Faker::Book.title` はなに？？と思うかもしれません。こちらはFakerで用意してくれている実装になります。
https://github.com/faker-ruby/faker/blob/main/doc/books/book.md

また`prefecture_cd`の13のようにハードコーディングすることもできます。一旦は簡単に説明のためにこうしておきます。`rails c`で実行してみましょう
```powershell
$ docker compose -f compose.yaml run --rm web rails c
[+] Creating 1/0
 ✔ Container docbase_rails-db-1  Running                                                                          0.0s 
Loading development environment (Rails 8.0.2)
docbase-rails(dev)> FactoryBot.create(:prefecture)
  TRANSACTION (0.5ms)  BEGIN /*application='DocbaseRails'*/
  Prefecture Create (6.5ms)  INSERT INTO `prefectures` (`prefecture_cd`, `name`, `created_at`, `updated_at`) VALUES ('13', 'Carrion Comfort', '2025-06-11 12:11:32.276846', '2025-06-11 12:11:32.276846') /*application='DocbaseRails'*/
  TRANSACTION (8.8ms)  COMMIT /*application='DocbaseRails'*/
=> 
#<Prefecture:0x0000796431c11ec0
 id: 2,
 prefecture_cd: "13",
 name: "Carrion Comfort",
 created_at: "2025-06-11 12:11:32.276846000 +0000",
 updated_at: "2025-06-11 12:11:32.276846000 +0000">
docbase-rails(dev)> 
```
実行結果が上記になります。`name`には`Carrion Comfort`  という謎の文字列が入っています。これがFakerにより自動で作られる値になります。ハードコーディングした`13` も当て込まれてますね。factoryの記述は実際にテストで呼び出す時にテーブルの一意制約やモデルのバリデーションに注意して作りましょう。

次にモデルのテストです。
```spec/models/prefecture_spec.rb
require 'rails_helper'

RSpec.describe Prefecture, type: :model do
  describe 'create' do
    let(:prefecture) { FactoryBot.build(:prefecture) }
    # 正常系のテスト
    context 'with valid attributes' do
      it 'is valid with a name and prefecture_cd' do
        expect(prefecture).to be_valid
      end
    end

    # 異常系のテスト
    context 'with invalid attributes' do
      it 'is invalid without a name' do
        prefecture.name = nil
        expect(prefecture).to be_invalid
      end

      # prefecture_cdがない場合のテスト
      it 'is invalid without a prefecture_cd' do
        prefecture.prefecture_cd = nil
        expect(prefecture).to be_invalid
      end

      # prefecture_cdが3文字以上の場合のテスト
      it 'is invalid with a prefecture_cd that has more than 3 characters' do
        prefecture.prefecture_cd = '123'
        expect(prefecture).to be_invalid
      end
    end
  end
end
```
modelファイルのバリデーションの境界値チェックなどすると良いかなと思っております。
テストを実行してみましょう。
```powershell
$ docker compose -f compose.yaml run --rm web rails spec
[+] Creating 1/0
 ✔ Container docbase_rails-db-1  Running                                                  0.0s 
/usr/local/bin/ruby -I/usr/local/bundle/gems/rspec-core-3.13.4/lib:/usr/local/bundle/gems/rspec-support-3.13.4/lib /usr/local/bundle/gems/rspec-core-3.13.4/exe/rspec --pattern spec/\*\*\{,/\*/\*\*\}/\*_spec.rb
....

Finished in 0.26719 seconds (files took 0.71844 seconds to load)
4 examples, 0 failures
```
テストが成功しました！
最後にコントローラーのテストです。
コントローラーのテストはspec/requests配下に作ることが多いです。
```spec/requests/api/v1/prefectures_spec.rb
require 'rails_helper'

RSpec.describe 'Api::V1::Prefectures', type: :request do
  let!(:prefecture) { create(:prefecture, name: 'Tokyo', prefecture_cd: '01') }
  let(:valid_attributes) { { name: 'Osaka', prefecture_cd: '02' } }
  let(:invalid_attributes) { { name: '', prefecture_cd: '' } }

  describe 'GET /api/v1/prefectures' do
    it 'returns all prefectures' do
      get api_v1_prefectures_path
      json_response = JSON.parse(response.body)

      expect(response).to have_http_status(:success)
      expect(json_response['data'].size).to eq(Prefecture.count)
    end
  end

  describe 'POST /api/v1/prefectures' do
    context 'with valid attributes' do
      it 'creates a new prefecture' do
        expect {
          post api_v1_prefectures_path, params: { prefecture: valid_attributes }
        }.to change(Prefecture, :count).by(1)

        json_response = JSON.parse(response.body)
        expect(response).to have_http_status(:success)
        expect(json_response['data']['name']).to eq('Osaka')
      end
    end

    context 'with invalid attributes' do
      it 'returns an error' do
        post api_v1_prefectures_path, params: { prefecture: invalid_attributes }
        json_response = JSON.parse(response.body)

        expect(response).to have_http_status(:unprocessable_entity)
        expect(json_response['status']).to eq('ERROR')
      end
    end
  end

  describe 'PUT /api/v1/prefectures/:id' do
    context 'with valid attributes' do
      it 'updates the prefecture' do
        put api_v1_prefecture_path(prefecture), params: { prefecture: { name: 'Kyoto' } }
        json_response = JSON.parse(response.body)

        expect(response).to have_http_status(:success)
        expect(json_response['data']['name']).to eq('Kyoto')
      end
    end

    context 'with invalid attributes' do
      it 'returns an error' do
        put api_v1_prefecture_path(prefecture), params: { prefecture: invalid_attributes }
        json_response = JSON.parse(response.body)

        expect(response).to have_http_status(:unprocessable_entity)
        expect(json_response['status']).to eq('ERROR')
      end
    end
  end

  describe 'DELETE /api/v1/prefectures/:id' do
    it 'deletes the prefecture' do
      expect {
        delete api_v1_prefecture_path(prefecture)
      }.to change(Prefecture, :count).by(-1)

      json_response = JSON.parse(response.body)
      expect(response).to have_http_status(:success)
      expect(json_response['message']).to eq('Deleted the prefecture')
    end
  end
end
```
実行してみましょう。
```spec/requests/api/v1/prefectures_spec.rb
$ docker compose -f compose.yaml run --rm web rails spec
[+] Creating 1/0
 ✔ Container docbase_rails-db-1  Running                                                  0.0s 
/usr/local/bin/ruby -I/usr/local/bundle/gems/rspec-core-3.13.4/lib:/usr/local/bundle/gems/rspec-support-3.13.4/lib /usr/local/bundle/gems/rspec-core-3.13.4/exe/rspec --pattern spec/\*\*\{,/\*/\*\*\}/\*_spec.rb
......F.F.

Failures:

  1) Api::V1::Prefectures POST /api/v1/prefectures with invalid attributes returns an error
     Failure/Error: expect(response).to have_http_status(:unprocessable_entity)
       expected the response to have status code :unprocessable_entity (422) but it was :ok (200)
     # ./spec/requests/api/v1/prefectures_spec.rb:36:in 'block (4 levels) in <main>'

  2) Api::V1::Prefectures PUT /api/v1/prefectures/:id with invalid attributes returns an error
     Failure/Error: expect(response).to have_http_status(:unprocessable_entity)
       expected the response to have status code :unprocessable_entity (422) but it was :ok (200)
     # ./spec/requests/api/v1/prefectures_spec.rb:58:in 'block (4 levels) in <main>'

Finished in 0.86106 seconds (files took 1.09 seconds to load)
10 examples, 2 failures

Failed examples:

rspec ./spec/requests/api/v1/prefectures_spec.rb:32 # Api::V1::Prefectures POST /api/v1/prefectures with invalid attributes returns an error
rspec ./spec/requests/api/v1/prefectures_spec.rb:54 # Api::V1::Prefectures PUT /api/v1/prefectures/:id with invalid attributes returns an error

/usr/local/bin/ruby -I/usr/local/bundle/gems/rspec-core-3.13.4/lib:/usr/local/bundle/gems/rspec-support-3.13.4/lib /usr/local/bundle/gems/rspec-core-3.13.4/exe/rspec --pattern spec/\*\*\{,/\*/\*\*\}/\*_spec.rb failed
```
エラーで落ちましたね。どうやら新規作成と更新処理でHTTPレスポンスエラーになってるとのことです。実装を確認してみましょう。
```app/controllers/api/v1/prefectures_controller.rb
def create
  prefecture = Prefecture.new(prefecture_params)
  if prefecture.save
    render json: { status: "SUCCESS", data: prefecture }
  else
    render json: { status: "ERROR", data: prefecture.errors }
  end
end

def update
  if @prefecture.update(prefecture_params)
    render json: { status: "SUCCESS", message: "Updated the prefecture", data: @prefecture }
  else
    render json: { status: "SUCCESS", message: "Not updated", data: @prefecture.errors }
  end
end
```
ここの処理ですね。
テストで指摘された`unprocessable_entity（HTTPステータスコード422）`を明記する必要がある理由は、リクエストがサーバーに到達したものの、リクエスト内容がサーバー側で処理できない場合に適切なエラーレスポンスを返すためです。

具体的には、リクエストのデータが不正である場合（例えば、必須フィールドが欠けている、またはフォーマットが正しくない場合）に、クライアントにその情報を伝えるために使用されます。現在の実装では、失敗時にHTTPステータスコード`200（成功）`を返しているため、クライアントがエラーを正しく認識できていませんでした。

実装を修正してみましょう。
```app/controllers/api/v1/prefectures_controller.rb
def create
  prefecture = Prefecture.new(prefecture_params)
  if prefecture.save
    render json: { status: "SUCCESS", data: prefecture }
  else
    render json: { status: "ERROR", data: prefecture.errors }, status: :unprocessable_entity
  end
end

def update
  if @prefecture.update(prefecture_params)
    render json: { status: "SUCCESS", message: "Updated the prefecture", data: @prefecture }
  else
    render json: { status: "SUCCESS", message: "Not updated", data: @prefecture.errors }, status: :unprocessable_entity
  end
end
```
実装を直しました。再度テストを実行してみましょう。
```powershell
$ docker compose -f compose.yaml run --rm web rails spec
[+] Creating 1/0
 ✔ Container docbase_rails-db-1  Running                                                  0.0s 
/usr/local/bin/ruby -I/usr/local/bundle/gems/rspec-core-3.13.4/lib:/usr/local/bundle/gems/rspec-support-3.13.4/lib /usr/local/bundle/gems/rspec-core-3.13.4/exe/rspec --pattern spec/\*\*\{,/\*/\*\*\}/\*_spec.rb
........F.

Failures:

  1) Api::V1::Prefectures PUT /api/v1/prefectures/:id with invalid attributes returns an error
     Failure/Error: expect(json_response['status']).to eq('ERROR')
     
       expected: "ERROR"
            got: "SUCCESS"
     
       (compared using ==)
     # ./spec/requests/api/v1/prefectures_spec.rb:59:in 'block (4 levels) in <main>'

Finished in 0.81308 seconds (files took 0.80466 seconds to load)
10 examples, 1 failure

Failed examples:

rspec ./spec/requests/api/v1/prefectures_spec.rb:54 # Api::V1::Prefectures PUT /api/v1/prefectures/:id with invalid attributes returns an error

/usr/local/bin/ruby -I/usr/local/bundle/gems/rspec-core-3.13.4/lib:/usr/local/bundle/gems/rspec-support-3.13.4/lib /usr/local/bundle/gems/rspec-core-3.13.4/exe/rspec --pattern spec/\*\*\{,/\*/\*\*\}/\*_spec.rb failed
```
createのテストは通るようになりましたが、まだupdateのテストがエラーですね。今度はERRORという文字を期待しているが、SUCCESSが返ってくるとなっています。実装を見てみましょう。
```app/controllers/api/v1/prefectures_controller.rb
def update
  if @prefecture.update(prefecture_params)
    render json: { status: "SUCCESS", message: "Updated the prefecture", data: @prefecture }
  else
    render json: { status: "SUCCESS", message: "Not updated", data: @prefecture.errors }, status: :unprocessable_entity
  end
end
```
statusの文字が`"SUCCESS"`になっていますね。これも実装の間違えで正しくは`"ERROR"`ですね。
```app/controllers/api/v1/prefectures_controller.rb
def update
  if @prefecture.update(prefecture_params)
    render json: { status: "SUCCESS", message: "Updated the prefecture", data: @prefecture }
  else
    render json: { status: "ERROR", message: "Not updated", data: @prefecture.errors }, status: :unprocessable_entity
  end
end
```
実装を直して、再度テストをします。
```powershell
$ docker compose -f compose.yaml run --rm web rails spec
[+] Creating 1/0
 ✔ Container docbase_rails-db-1  Running                                                  0.0s 
/usr/local/bin/ruby -I/usr/local/bundle/gems/rspec-core-3.13.4/lib:/usr/local/bundle/gems/rspec-support-3.13.4/lib /usr/local/bundle/gems/rspec-core-3.13.4/exe/rspec --pattern spec/\*\*\{,/\*/\*\*\}/\*_spec.rb
..........

Finished in 0.67078 seconds (files took 0.75677 seconds to load)
10 examples, 0 failures
```
おめでとうございます。テストがすべて通りましたね。
このようにして実装とテストを見比べて品質向上をしていきます。


以上で、この記事を終わります。
ありがとうございました。

## 補足
railsコマンド等で生成されたファイルが権限の理由で上書きできないなどの場合は以下を実行してみてください。
```powershell
$ sudo chown -R あなたのシステムの名前:あなたのシステムの名前 .
```