Rails Tutorialの知識から【ポートフォリオ】を作って勉強する話 #15 投稿機能, Active Storage編

# こんな人におすすめ
* プログラミング初心者でポートフォリオの作り方が分からない
* Rails Tutorialをやってみたが理解することが難しい

前回：[#14 ユーザ投稿表示, ページネーション編](https://qiita.com/aokyo17/items/cb2ca3f320462a91c93b)
次回：[#16 線グラフ, Chartkick編](https://qiita.com/aokyo17/items/f5b770e490faa15e6a5b)

# 今回の流れ
1. ルーティングと各アクションを整える
* 投稿フォームをつくる
* 画像を投稿できるようにする
* 投稿編集・削除機能をつくる
* 投稿時の不具合を解消する
* テストをつくる

この記事は、**動画を観た時間を記録するアプリのポートフォリオ**です。
今回は**投稿機能**を実装します。
（投稿のためのモデルが必要なので未読の方は[#14](https://qiita.com/aokyo17/items/cb2ca3f320462a91c93b)を参照下さい。）

# 1. ルーティングと各アクションを整える
投稿フォームはshow.html.erbと同じビューを使用します。
投稿編集と削除の際には別途ビューを生成します。
以上から必要なアクションはcreate、edit、update、destroyとなります。
よって、ここでの手順は以下の通りです。

* ルーティングを行う
* 作成済みのメソッドを移動する
* 各アクションを整える

## ルーティングを行う 
まずはルーティングを行いましょう。

```rb:config/routes.rb
Rails.application.routes.draw do
  # 中略
  resources :microposts, only: [:create, :edit, :update, :destroy]
end
```

## 作成済みのメソッドを移動する
各アクションを整える前に、1つ変更を加えます。
logged_in_userメソッドはMicropostsコントローラでも使用します。
先にこちらはApplicationコントローラに移動しましょう。

```rb:app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
  protect_from_forgery with: :exception
  include SessionsHelper
  
  private
    def logged_in_user
      unless logged_in?
        store_location
        flash[:warning] = 'ログインしてください'
        redirect_to login_url
      end
    end
end
```

## 各アクションを整える
create、edit、update、destroyといった4つのアクションを整えましょう。

```app/controllers/microposts_controller.rb
class MicropostsController < ApplicationController
  before_action :logged_in_user, only: [:create, :edit, :update, :destroy]
  before_action :correct_user, only: :destroy
  
  def create
    @user = current_user
    @micropost = current_user.microposts.build(micropost_params) if logged_in?
    @microposts = @user.microposts.page(params[:page]).per(10)
    
    if @micropost.save
      redirect_to current_user
    else
      render 'users/show'
    end
  end
  
  def edit
    @micropost = current_user.microposts.find_by(id: params[:id]) || nil
    if @micropost.nil?
      flash[:warning] = "編集権限がありません"
      redirect_to root_url
    end
  end
  
  def update
    @micropost = current_user.microposts.find_by(id: params[:id])
    @micropost.update_attributes(micropost_params)
    if @micropost.save
      flash[:success] = "編集が完了しました"
      redirect_to current_user
    else
      render 'microposts/edit'
    end
  end
  
  def destroy
    @micropost.destroy
    flash[:success] = "ログが削除されました"
    redirect_to current_user
  end
  
  private
    def micropost_params
      params.require(:micropost).permit(:memo, :time, :picture)
    end
    
    def correct_user
      @micropost = current_user.microposts.find_by(id: params[:id])
      redirect_to root_url if @micropost.nil?
    end
end
```

このポートフォリオは**Tutorialと異なり、user_pathにフォームを置いています。**
この場合、フォーム入力失敗時のパスがmicroposts_pathとなります。

その結果、フォーム入力失敗時に/user/show.html.erbをrenderするため、Micropostsコントローラの**createアクションにuserとmicroposts変数を渡す必要があります。**

# 2. 投稿フォームをつくる
投稿フォームをつくりましょう。
パーシャルを生成し、そこに投稿フォームを作ります。

```bash:bash
$ touch app/views/layouts/_micropost_form.html.erb
```

```erb:app/views/layouts/_micropost_form.html.erb
<div class="container micropost-container">
  <h1>Form</h1>
  <%= form_with(model: @micropost, url: microposts_path, local: true) do |form| %>
    <%= render 'shared/error_messages', object: form.object %>
    <div class="form-group">
      <%= form.number_field :time, class: 'form-control', placeholder: "時間（分）を入力してください" %>
    </div>
    <div class="form-group">
      <%= form.text_area :memo, class: 'form-control', placeholder: "メモを加えてください" %>
    </div>
    <div class="form-group">
      <%= form.file_field :picture %>
    </div>
    <div class="form-group">
      <%= form.submit "記録する", class: 'btn btn-info btn-lg form-submit' %>
    </div>
  <% end %>
</div>
```

![lantern_form.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/454653/e8614abc-31e3-3684-2cff-16b9c6a4e925.png)

まず説明する点は、このような記述についてです。

```erb:
<%= render 'shared/error_messages', object: form.object %>
```

renderにobjectオプションを使うことにより、error_messages.html.erbで、userやmicropostなどを引き渡すオブジェクト名としてobjectを使用できます。
**エラーを表示する他のビューに対しても、同様に記述しましょう。**

加えてerror_messages.html.erbはこのように変更しましょう。

```erb:app/views/shared/_error_messages.html.erb
<% if object.errors.any? %>
  <div id="error_explanation">
    <div class="alert alert-danger alert-form-extend" role="alert">
      <%= object.errors.count %>個のエラーがあります
    </div>
    <ul>
      <% object.errors.full_messages.each do |msg| %>
        <li><%= msg %></li>
      <% end %>
    </ul>
  </div>
<% end %>
```

参考になりました↓
[form_for の f.object って何だ？（Rails)](https://mon-sele.com/post18/)

続いてこの部分ですが、現状では動作しません。

```erb:
<%= form.file_field :picture %>
```

こちらは下記の画像投稿を可能にすることで動作します。

# 3. 画像を投稿できるようにする
画像投稿にはRails5.2から対応のActive Storageを使用します。
また画像をアップロードするストレージとしてS3を使用します。
まずはActive Storageについて理解しましょう。

## Active Storageを理解する
Active Storageとはファイルをアップロードする機能のことです。
以下の記事が分かりやすいですので一読してみましょう。
[【Rails 5.2】Active Storageの使い方](https://qiita.com/hmmrjn/items/7cc5e5348755c517458a)

加えて今回はアップロード時の保存先にS3を使用します。
S3とはAWSが提供するストレージのことです。
S3の使用にはGemの追加と設定の編集が必要です。

以上を踏まえてActive Storageを使用するにはこのような手順が必要です。

* S3（バケット）を作成する
* Active Storageを用意する
* Active StorageとS3を紐づける

## S3（バケット）を作成する
まずはS3（バケット）を作成しましょう。
ここでの手順は以下の通りです。

* IAMユーザに権限を与える
* アクセスキーを作成する
* バケットを作成する

### IAMユーザに権限を与える
ポートフォリオ用のユーザにS3を使用する権限を与えます。
以下の手順を行いましょう。

1. ルートユーザでAWSにログインする
2. IAMを開く
3. ポートフォリオ用ユーザを選択する
4. 「アクセス権限の追加」を押す
5. 「既存のポリシーを直接アタッチ」からS3を検索する
6. 「AmazonS3FullAccess」をチェックし確認する

分からない場合はこちらをご覧ください↓
[【AWS】【S3】作成手順 ＆ アップロード手順 ＆ アクセス権限設定手順](https://go-journey.club/archives/9653)

### アクセスキーを作成する
続いて後述で必要になるアクセスキーを作成します。
このキーを作成することでRailsとS3を紐づけることが可能です。
以下の手順を行いましょう。

1. ルートユーザでAWSにログインする
2. IAMを開く
3. ポートフォリオ用ユーザを選択する
4. 「認証情報」タブから「アクセスキーの作成」を押す
5. アクセスキーとシークレットアクセスキーをメモする
**（一度しか表示されないので注意）**

### バケットを作成する
最後にポートフォリオ用ストレージとなるバケットを作成します。
以下の手順を行いましょう。

1. IAMユーザでAWSにログインする
2. S3を開く
3. 「バケットを作成する」を押す
4. バケット名を入力し東京リージョンを選択する
4. 「次へ」を押し続け「バケットを作成」を押す

分からない場合はこちらをご覧ください↓
[【AWS】【S3】作成手順 ＆ アップロード手順 ＆ アクセス権限設定手順](https://go-journey.club/archives/9653)
以上でS3（バケット）の作成は完了です。

## Active Storageを用意する
次にActive Storageを用意しましょう。
ここでの手順は以下の通りです。

* Active Storageをインストールする
* モデル（今回はMicropostモデル）を編集する

### Active Storageをインストールする
まずはインストールを行いDBをマイグレートします。

```bash:bash
$ rails active_storage:install
$ rails db:migrate
```

### モデル（今回はMicropostモデル）を編集する
モデルの編集はいたって簡単です。
app/models/micropost.rbに以下を加えるだけで成立します。

```rb:app/models/micropost.rb
has_one_attached :picture
```

ここで宣言した名前（今回はpicture）がモデルのカラムとして機能します。
以上でActive Storageの用意は完了です。

## Active StorageとS3を紐づける
最後にActive StorageとS3を紐づけましょう。
ここでの手順は以下の通りです。

* 必要なGemを追加する
* 設定を編集する

### 必要なGemを追加する
S3との紐づけにはaws-sdk-s3が必要です。
Gemfileに追加しましょう。

```diff:gemfile
+ gem 'aws-sdk-s3'
```

### 設定を編集する
Active Storageの保存先をS3にするため:localから:amazonに変更します。

```rb:config/environments/development.rb
  # 中略
  # Store uploaded files on the local file system (see config/storage.yml for options)
  config.active_storage.service = :amazon
```

```rb:config/environments/production.rb
  # 中略
  # Store uploaded files on the local file system (see config/storage.yml for options)
  config.active_storage.service = :amazon
```

続いてstorage.ymlにてamazon:の部分をこのように変更します。

```yaml:config/storage.yml
# Use rails credentials:edit to set the AWS secrets (as aws:access_key_id|secret_access_key)
amazon:
  service: S3
  access_key_id: <%= Rails.application.credentials.dig(:aws, :access_key_id) %>
  secret_access_key: <%= Rails.application.credentials.dig(:aws, :secret_access_key) %>
  region: ap-northeast-1
  bucket: #S3で作成したバケット名（今回はlantern-lantern-s3）
```

最後に、先ほどメモしたアクセスキーとシークレットアクセスキーを入力します。
この2つはセキュリティ上Rails Credentialsを使用します。
この機能により直接キーを入力することを回避し暗号化します。

それではRails Credentialsを編集しましょう。
エディターにはVimを使用します。

```bash:bash
$ EDITOR=vim rails credentials:edit
```

Vimは癖があるので、下記に簡単な操作を載せておきます。

* 入力開始：i
* 入力終了：esc
* 保存終了：ZZ
* 保存せず終了：:q!

```yml
aws:
 access_key_id: # ここにアクセスキーを入力
 secret_access_key: # ここにシークレットアクセスキーを入力
```

以上で紐づけは完了です。

## 画像をリサイズする
このままではユーザが投稿する画像サイズに制限がありません。
画像のリサイズにはMiniMagickというGemを使用しましょう。
ここでの手順は以下の通りです。

* Gemを追加する
* ImageMagickをインストールする
* リサイズのメソッドを定義する
* ビューで表示する

### Gemを追加する

```diff:gemfile
+ gem 'mini_magick'
```

```bash:bash
$ bundle install
```

### ImageMagickをインストールする
MiniMagickの使用にはImageMagickのインストールが必要です。
こちらも加えて行いましょう。

```bash:bash
$ sudo yum install -y ImageMagick
```

### リサイズのメソッドを定義する
画像のリサイズを行うよう、モデルにメソッドを定義します。

```rb:app/models/micropost.rb
def resize_picture
  return self.picture.variant(resize: '100x100').processed
end
```

### ビューで表示する
リサイズされた画像を表示するにはこのように記述します。

```erb:app/views/layouts/_log.html.erb
<% @microposts.each do |micropost| %>
<!-- 省略 -->
  <% if micropost.picture.attached? %>
  <div class="log-picture">
    <%= image_tag micropost.resize_picture %>
  </div>
  <% end %>
<!-- 省略 -->
<% end %>
```

フォーム入力↓
<img width="1280" alt="lantern_input_form.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/454653/07badafd-0840-fd05-a8ac-519642d4994b.png">
ログ表示↓
<img width="1280" alt="lantern_output_form.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/454653/5bee78e5-40e3-cd98-c60f-9064e71f14ef.png">
以上で画像のリサイズは完了です。

# 4. 投稿編集・削除機能をつくる
編集と削除のためのアクションはすでに終えました。
よって、ここでの手順はビューをつくるのみです。

```bash:bash
$ touch app/views/microposts/edit.html.erb
```

```erb:app/views/microposts/edit.html.erb
<% provide(:title, 'メモ編集') %>
<div class="container micropost-edit-container">
  <div class="edit-titles">
    <%= image_tag 'edit_02.png', class: 'edit-img' %>
    <h1 class="title edit-micropost-title">メモ編集</h1>
  </div>
  <%= form_with(model: @micropost, url: micropost_path, local: true) do |form| %>
    <%= render 'shared/error_messages', object: form.object %>
    <div class="form-group">
      <%= form.label :time, '時間' %>
      <%= form.number_field :time, class: 'form-control', placeholder: @micropost.time %>
    </div>
    <div class="form-group">
      <%= form.label :memo, 'メモ' %>
      <%= form.text_area :memo, class: 'form-control', placeholder: @micropost.memo %>
    </div>
    <div class="form-group">
      <%= form.label :picture, '画像' %>
      <%= form.file_field :picture, class: 'form-control-file', placeholder: @micropost.picture %>
    </div>
    <div class="row">
      <div class="col">
        <div class="form-group">
          <%= link_to "削除", micropost_path, method: :delete, data: { confirm: "本当に削除しますか？" }, class: 'btn btn-lg btn-danger btn-edit-user' %>
        </div>
      </div>
      <div class="col">
        <div class="form-group">
          <%= form.submit "編集", class: 'btn btn-info btn-lg btn-edit-user' %>
        </div>
      </div>
    </div>
    <div class="form-group">
      <%= link_to "戻る", current_user, class: 'btn btn-lg btn-edit-user btn-back' %>
    </div>
  <% end %>
</div>
```

メモ編集画面↓
<img width="1280" alt="lantern_edit_micropost.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/454653/002e8d80-fc3d-2530-1322-eadde0178890.png">

# 5. 投稿時の不具合を解消する
このままではフォームの時間が空の時、ログが「分」のみで出力されてしまいます。
それを避けるために簡単な条件を書き、ログを正しく出力しましょう。

```erb:app/views/layouts/_log.html.erb
<!-- 省略 -->
<% @microposts.each do |micropost| %>
    <li id ="micropost-<%= micropost.id %>">
      <span class="row log-list">
        <span class="col-2 log-timestamp d-none d-md-inline-block log-timestamp-block">
          <span class="log-timestamp"><%= time_ago_in_words(micropost.created_at) %>前</span>
        </span>
        <span class="col-md-10 col-log-memos">
          <div class="log-time-and-edit">
            <div class="row">
              <span class="log-time col-3">
                <% if micropost.time.nil? %>
                  0分
                <% else %>
                  <%= micropost.time %>分
                <% end %>
              </span>
              <span class="col-7 log-timestamp log-timestamp-inline"><%= time_ago_in_words(micropost.created_at) %>前</span>
              <span class="log-edit col-2"><%= link_to image_tag('edit.png', class: "log-edit-image"), edit_micropost_path(micropost) %></span>
            </div>
          </div>
          <% if micropost.memo.present? %>
            <div class="log-memo"><%= micropost.memo %></div>
          <% end %>
          <% if micropost.picture.attached? %>
            <div class="log-picture"><%= image_tag micropost.resize_picture %></div>
          <% end %>
        </span>
      </span>
    </li>
<% end %>
<!-- 省略 -->
```

時間が空の場合「0分」と表示↓
（少々デザインも変更しています）
![lantern_log_correction.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/454653/733d5f05-128f-e6bf-b38b-98d0db1ee581.png)

# 6. テストをつくる
残るはテストのみです。
ここではModel specとRequest specのテストを行います。

## Model specでのテスト
このテストでは以下を確認します。

* pictureのみ存在するMicropostモデルは有効か
* 5MBを超える画像は無効か
* 画像以外のファイルは無効か

その前に5MB以上の画像、画像以外のファイルを以下のフォルダに準備しましょう。

```bash:bash
$ mkdir spec/fixtures/images
```

```rb:
# 実際にファイルを追加する
```

その後、テストを記述します。

```rb:spec/models/micropost_spec.rb
require 'rails_helper'

RSpec.describe Micropost, type: :model do
  
  let(:user) { create(:user) }
  let(:micropost) { user.microposts.build(time: 240, memo: "Lorem ipsum", user_id: user.id) }

  # 中略

  describe "picture" do
    it "should be valid if all columns are nil except picture" do
      micropost.update_attributes(time: nil, memo: nil, user_id: user.id)
      expect(micropost).to be_invalid
      micropost.picture.attach(io: File.open(Rails.root.join('spec', 'fixtures', 'images', 'test.jpg')), filename: 'test.jpg', content_type: 'image/jpg')
      expect(micropost).to be_valid
    end
    
    it "should not be over than 5MB" do
      micropost.picture.attach(io: File.open(Rails.root.join('spec', 'fixtures', 'images', 'test_5mb.jpg')), filename: 'test_5mb.jpg', content_type: 'image/jpg')
      expect(micropost).to be_invalid
    end
    
    it "should be only images file" do
      micropost.picture.attach(io: File.open(Rails.root.join('spec', 'fixtures', 'images', 'test.pdf')), filename: 'test.pdf', content_type: 'application/pdf')
      expect(micropost).to be_invalid
    end
  end
end
```

参考にさせていただきました↓
[Active Storageの簡単なバリデーションの実装とテスト](https://shuttodev.hatenablog.com/entry/2019/09/04/015756)
[ActiveStorageでattachできるものについて調べてみた](https://rit-inc.hatenablog.com/entry/2018/04/02/160106)
[Ruby IOクラスについて学ぶ](https://qiita.com/naotospace/items/bd0a5295e540e27936b3)
[Railsのファイルパスの操作のメモ](https://qiita.com/munaita_/items/e53e0e06e5537e1900ca)

## Request specでのテスト
このテストでは以下を確認します。

* ログインしていないユーザの投稿が無効か
* フォームが全て空欄のユーザ投稿が無効か
* ログインしているユーザの投稿が有効か
* ログインしていないユーザの投稿削除が無効か
* 他ユーザの投稿削除が無効か
* ログインしているユーザの投稿削除が有効か
* ログインしていないユーザの投稿編集が無効か
* 他ユーザの投稿編集が無効か
* フォームが全て空欄の投稿編集が無効か
* ログインしているユーザの投稿編集が有効か

```rb:spec/requests/microposts_spec.rb
require 'rails_helper'

RSpec.describe "Microposts", type: :request do
  
  let(:user) { create(:user) }
  let(:other_user) { create(:other_user) }
  
  def post_valid_information
    post microposts_path, params: { micropost: { memo: "aaa" } }
  end
  
  def post_invalid_information
    post microposts_path, params: { micropost: { memo: nil } }
  end
  
  def patch_valid_information
    patch micropost_path, params: { micropost: { memo: "bbb" } }
  end
  
  def patch_invalid_information
    patch micropost_path, params: { micropost: { memo: nil } }
  end
  
  describe "POST /microposts" do
    it "does not add a micropost when not logged in" do
      expect{ post_valid_information }.not_to change(Micropost, :count)
      follow_redirect!
      expect(request.fullpath).to eq '/login'
    end
    
    it "does not add a micropost when the form has no information" do
      log_in_as(user)
      get user_path(user)
      expect{ post_invalid_information }.not_to change(Micropost, :count)
    end
    
    it "succeeds to add a micropost" do
      log_in_as(user)
      get user_path(user)
      expect(request.fullpath).to eq '/users/1'
      expect{ post_valid_information }.to change(Micropost, :count).by(1)
      follow_redirect!
      expect(request.fullpath).to eq '/users/1'
    end
  end
  
  describe "DELETE /micropost" do
    it "does not destroy a micropost when not logged in" do
      delete micropost_path(1)
      follow_redirect!
      expect(request.fullpath).to eq '/login'
    end
    
    it "does not destroy a micropost when other users logged in" do
      log_in_as(user)
      get user_path(user)
      post_valid_information
      follow_redirect!
      delete logout_path
      log_in_as(other_user)
      get user_path(other_user)
      expect(request.fullpath).to eq '/users/2'
      post_valid_information
      expect{ delete micropost_path(1) }.not_to change(Micropost, :count)
      expect{ delete micropost_path(2) }.to change(Micropost, :count).by(-1)
    end
    
    it "succeeds to destroy a micropost" do
      log_in_as(user)
      get user_path(user)
      expect{ post_valid_information }.to change(Micropost, :count).by(1)
      follow_redirect!
      expect{ delete micropost_path(1) }.to change(Micropost, :count).by(-1)
      follow_redirect!
      expect(request.fullpath).to eq '/users/1'
      expect(flash[:success]).to be_truthy
    end
  end
  
  describe "GET /microposts/:id/edit" do
    it "does not edit a micropost when not logged in" do
      log_in_as(user)
      get user_path(user)
      post_valid_information
      follow_redirect!
      delete logout_path
      follow_redirect!
      get edit_micropost_path(1)
      follow_redirect!
      expect(request.fullpath).to eq '/login'
    end
    
    it "does not edit a micropost when other users logged in" do
      log_in_as(user)
      get user_path(user)
      post_valid_information
      follow_redirect!
      delete logout_path
      follow_redirect!
      log_in_as(other_user)
      get edit_micropost_path(1)
      follow_redirect!
      expect(request.fullpath).to eq '/'
    end
    
    it "does not edit a micropost when the form has no information" do
      log_in_as(user)
      get user_path(user)
      post_valid_information
      follow_redirect!
      get edit_micropost_path(1)
      expect(request.fullpath).to eq '/microposts/1/edit'
      patch_invalid_information
      expect(request.fullpath).to eq '/microposts/1'
    end
    
    it "succeeds to edit a micropost" do
      log_in_as(user)
      get user_path(user)
      post_valid_information
      follow_redirect!
      get edit_micropost_path(1)
      expect(request.fullpath).to eq '/microposts/1/edit'
      patch_valid_information
      follow_redirect!
      expect(request.fullpath).to eq '/users/1'
    end
  end
end
```

以上でテストは終了です。
お疲れさまでした。

備考：Active Storageを伴うテストにFactoryBotを使いたい
---
Micropostモデルを検証する際、FactoryBotを使ったテストを行いたかったのですが、断念しました。一応調べた分を共有します。

Active Storageを伴うFactoryBotを使う手順は以下の通りです。

1. 環境設定を行う
2. FactoryBotを整える
3. テストを書く

## 環境設定を行う
まずは、ダミーでアップロードを行うfixture_file_uploadメソッドを使うために、環境設定をしましょう。

```rb:spec/rails_helper.rb
# 中略
RSpec.configure do |config|
  # 中略
  # factoryBot内での呼び出し
  FactoryBot::SyntaxRunner.class_eval do
    include ActionDispatch::TestProcess
  end
  # fixtureのパス指定（テスト時のパスをfixtures以下から省略できる）
  config.fixture_path = "#{::Rails.root}/spec/fixtures"
# 中略
end
```

参考にさせていただきました↓
[parperclipでファイルアップロードをRspecでテスト w/ factory_girl](https://konyu.hatenablog.com/entry/2015/05/05/155421)
[Factory Bot trait for attaching Active Storange has_attached](https://blog.eq8.eu/til/factory-bot-trait-for-active-storange-has_attached.html)

## FactoryBotを整える
続いてFactoryBotを整えます。

```rb:spec/factories/microposts.rb
FactoryBot.define do
  factory :micropost do
    trait :memo_1 do
      time { 240 }
      memo { "I just ate an orange!" }
      user_id { 1 }
      created_at { 10.minutes.ago }
      picture { fixture_file_upload('/images/test.jpg', 'image/jpg') }
    end
    # 中略
    association :user
  end
end
```

参考にさせていただきました↓
[Factory Bot trait for attaching Active Storange has_attached](https://blog.eq8.eu/til/factory-bot-trait-for-active-storange-has_attached.html)

## テストを書く
残るはletでMicropostモデルを定義するだけです。

```rb:spec/models/micropost_spec.rb
require 'rails_helper'

RSpec.describe Micropost, type: :model do
  let(:micropost) { create(:micropost, :memo_1) }
  # 中略
end
```

しかしテスト時に画像を追加・削除する際、Active Storageで生成される他の2つのモデルとの関連づけがないためエラーが発生します。

```rb:
# どれもエラーが発生する
micropost.picture = nil
micropost.save!

micropost.update_attribute(:picture, nil)

micropost.picture.attach(nil)
```

```
ActiveRecord::RecordNotSaved:
       Failed to save the new associated picture_attachment.
```

強引に2つのクラスを生成するという方法もあるのですが、断念しました。
シンプルにテストを通す方法をご存知の方は、ご教示お願いします。

参考にさせていただきました↓
[ruby on rails 画像 fixture_file_uploadに{file}が存在しないというエラーがあります](https://code-examples.net/ja/q/8980e1)
[ruby on rails rails_blob_path ActiveStorageでモデルテストを正しく行うには?](https://code-examples.net/ja/q/2d6128b)

---

前回：[#14 ユーザ投稿表示, ページネーション編](https://qiita.com/aokyo17/items/cb2ca3f320462a91c93b)
次回：[#16 線グラフ, Chartkick編](https://qiita.com/aokyo17/items/f5b770e490faa15e6a5b)