ActiveStorageでテストがうまくいかない~のでとりあえず対処したメモ

ActiveStorageを使ったModelのテストがうまくいかない!となったので、対処法のメモをば。 テスト環境はRSpec + factory_bot でつくっています。

has_one_attached をもつ Hoge Modelがあるとして、

# hoge.rb
class Hoge < ApplicationRecord
  has_one_attached :image
end

ファイルがアップロードされたHoge Modelが欲しくなることがありまして、テストデータの定義spec/factories/hoge.rbを以下のように書きました。

# spec/factories/hoge.rb
FactoryBot.define do
  factory :hoge do
    after(:create) do |hoge|
      File.open('image.png') do |f|
        hoge.image.attach(io: f, filename: "#{hoge.id}.png", content_type: 'image/png')
      end
    end
  end
end

ちなみに、テスト時の ActiveStorage の挙動はテストモードにしておきます。

# config/environments/test.rb
config.active_storage.service = :test

(テスト項目ごとに毎度ファイルをアップロードする相当のことを行っているのがイマイチかもなぁと思いつつも)attach メソッドでファイルを都度アップロードすることで、spec内で create(:hoge) したときにファイルがアップロードされ、各種テストが実行できることを期待しました。しかし、なぜか ActiveStorage::IntegrityError が大量に出てしまったのでした。

RailsのIssueを見てみると似たような症状をコメントしている方もいるような。

Upload same file multiple time give uninitialized constant ActiveStorage::IntegrityError · Issue #32855 · rails/rails · GitHub

いずれにしても解決策がわからん!となったので、対処療法的に以下のような実装をしてみました。

まず、ActiveStorageで使っている ActiveStorage::Attachment , AcitiveStorage::Blob Modelのfactoryを定義します。

# spec/factories/active_storage/attachment.rb
FactoryBot.define do
  factory :attachment, class: ActiveStorage::Attachment do
    name ''
    record_type ''
    record_id 0
    association :blob, factory: :blob
  end
end
# spec/factories/active_storage/blob.rb
FactoryBot.define do
  factory :blob, class: ActiveStorage::Blob do
    sequence(:key) { SecureRandom.urlsafe_base64 }  # ユニークなキーでなければならない
    filename 'test_image.png'
    content_type 'image/png'
    metadata ''
    byte_size 13317
    checksum 'R-EvpP3cJEEQCSwQq/FRCF=='

    after(:create) do |blob|
      path = blob.service.send(:path_for, blob.key)
      FileUtils.mkdir_p(File.dirname(path))
      FileUtils.copy_file("#{Rails.root}/spec/images/test_image.png", path) # テストファイル image.png をファイルアップロード後の場所に移動する
    end
  end
end

byte_sizechecksum が決め打ちなど、いろいろと問題のある定義ですが、ひとまずはテストを動かせるようにしたいので目をつむっていただきたく...。byte_sizechecksumdevelopment 環境で実際にファイルをアップロードしたときに生成される active_storage_blobs テーブルのレコード値をそのまま使っています。traitなどを使ってリファクタリングすれば決め打ちでもよいかもしれないと思っています。)

こうすることで、Attachment Modelがつくられたときに Blob Modelもつくられ、Blobに紐づく実ファイルもあるべき場所にコピーされます。

Attachment Modelで has_one_attached を持つModelと Blob の紐づけを行うので、先ほどのspec/factories/hoge.rb を以下のように修正します。 ( has_one_attached :image でModelとアップロードファイルの関連を定義したのでnameには image を設定します。また、record_typeにModel名を、record_idにModelの主キーの値を設定します。)

# spec/factories/hoge.rb
FactoryBot.define do
  factory :hoge do
    after(:create) do |hoge|
     create(:attachment, name: 'image', record_type: 'Hoge', record_id: hoge.id)
    end
  end
end

これでひとまずはファイルがアップロードされた相当の環境を再現することができ、テストが通るようになりました。ただ、以下の点が気になるところです。

  • テスト時にコピーしたファイルの後処理(一応tmpディレクトリ配下にファイルは生成される)
  • アップロードファイルの配置場所を activestorage/lib/active_storage/service/disk_service.rbpath_for メソッドで取得しているが、Private Methodなので今後なくなったり名前が変わったりしないか心配

テストどうやればよいのかわからなくてこまった!という話でした。こうやればいいよ!などあればコメントなどいただけるととっても喜びます。

ではではー。