今までプロダクト優先で自動テストを書くのが億劫だったことを反省し、ついに意を決してRSpecを導入しました。
目標は
- 重要なモデルのバリデーションテスト
- コントローラー(request)の200確認・DB更新確認
- ボタンやリンク経由で画面遷移のシナリオテスト(フィーチャーテスト)
- CircleCIでRSpecのテストをデプロイ前に入れ込み、OKとならなければデプロイしないようにする
をすることです。もちろん究極は先にSpecを書いて、コードを実装するテストドリブン開発が理想ですが、今回後付になってしまったので現状の挙動を担保するという思想でテストを作成してます。
ただ、テストコードを書くと、意外なことにModelを端折ってしまって書いてないので、テストコード自体が動作しないというケースもあったりしてソース自体にも手を加える必要は出てきます。そうすると動いているソースに手を入れない的な昔からあるようなセオリーで押しきれないことがわかります。これもソースのメンテナンス性を上げるためのRSpecの効能と考えていいでしょう。
group :development, :test do
gem "rspec-rails" # RSpec本体
gem "factory_bot_rails" # FactoryBot
gem 'spring-commands-rspec' # RSpecのパフォーマンスを上げるGem
gem 'capybara', '~> 2.15.2' # フィーチャーテストのためのCapibara
gem 'rspec_junit_formatter' # CircleCI、JenkinsなどCIでSpec結果を読みやすくするためのGem
end
ちなみにRSpecは実行すると自動的に環境はRAILS_ENV=testになります。なので専用のデータベースをdevelopment用と別に作成する必要があります。またRails 5.1以降はテストで発生したデータを消すためのdatabase_cleanerは不要だそうです。これは5.1以降はテスト後にDBが自動的にRollbackしてデータを消すようになっているためだとか。データベースの作成に合わせてDBの設定にも以下を追加します。ちゃんとテスト用のDB名にしてあげるのがミソです。
# Warning: The database defined as "test" will be erased and
# re-generated from your development database when you run "rake".
# Do not set this db to the same as development or production.
test:
<<: *default
database: test_db
また、config/environments/test.rbも作成します。環境別固有設定を施すためです。
ほとんどdevelopment環境と一緒なのでconfig/environments/development.rbをコピーするだけですが、自分はweb_consoleを使っていてRSpecが動かないことがあったので以下の設定を追加しています。
# web_console
config.web_console.development_only = false
ここまでお膳立てしてようやく bundle
とジェネレーターで環境を整えます。
# RSpecインストール
$ bundle install
# 定型的な設定ファイルの作成
$ rails generate rspec:install
Running via Spring preloader in process 266
create .rspec
create spec
create spec/spec_helper.rb
create spec/rails_helper.rb
rails_helper.rbとspec_helper.rbの役割の違いはよくわからないですが、ほとんどのSpecファイルにrails_helper.rbへの読み込みが入るので、こちらにもっぱらヘルパーの設定を加えます。
今回はdevise使うのでそれ用のヘルパーとダミーデータ作成のためのFactoryBotに設定のヘルパーを加えています。コードの最後の方に追加しました。
config.include Devise::Test::IntegrationHelpers, type: :request
config.include FactoryBot::Syntax::Methods
Deviseを使用しているアプリケーションの場合、ヘルパー本体はこちらに書きます。
module RequestSpecHelper
include Warden::Test::Helpers
def self.included(base)
base.before(:each) { Warden.test_mode! }
base.after(:each) { Warden.test_reset! }
end
def sign_in(resource)
login_as(resource, scope: warden_scope(resource))
end
def sign_out(resource)
logout(warden_scope(resource))
end
private
def warden_scope(resource)
resource.class.name.underscore.to_sym
end
end
ベースとなる準備は以上です。
Seed-fuでマスターデータを用意
アプリケーションにもよると思いますが、ある程度マスターデータが用意されてないと凝ったテストが出来ないという人はFactoryBotではなくてSeedでDBに値を事前に入れておくのがいいかと思います。Railsに元からあるseedよりもseed-fuの方が使い勝手良さそうなのでこちらをインストールします。データの自動投入に汎用的に使えるGemです。
# Seed -fu
gem "seed-fu"
bundleで入れたら以下のファイルを作成します。
Spree::Core::Engine.load_seed if defined?(Spree::Core)
Spree::Auth::Engine.load_seed if defined?(Spree::Auth)
あとは投入データを作成します。投入するためのスクリプトはこちらです。これがそのままDBの定義とseedの実行母体になります。
require 'csv'
csv = CSV.read('db/fixtures/test/001_areas.csv')
csv.each do |csvdata|
Area.seed do |s|
s.id = csvdata[0]
s.prefstate_id = csvdata[1]
s.photo = csvdata[2]
s.lat = csvdata[3]
s.lng = csvdata[4]
s.created_at = csvdata[5]
s.updated_at = csvdata[6]
end
end
以下データ。
1,13,"",,,"2016-08-15 00:00:00","2016-08-15 00:00:00"
2,13,"",,,"2016-08-15 00:00:00","2016-08-15 00:00:00"
3,13,"",,,"2016-08-15 00:00:00","2016-08-15 00:00:00"
ちなみにdb/fixtures/の下のtestは環境名です。上記を設定したら
RAILS_ENV=test rails db:seed_fu
でデータ投入できますが、この際に環境ごとに投入データを変えられます。そのためにディレクトリで分けているのです。ちなみに実行はファイルの接頭辞の番号順に行われますので、外部キー制約など順番を考慮したい場合はうまく活用してください。
その他DB操作で便利なコマンド
RAILS_ENV=test rails db:reset #テーブル作り直し
RAILS_ENV=test bundle exec rake db:schema:load #テーブル作り直し
RAILS_ENV=test rails db:seed_fu FILTER=029_styles FIXTURE_PATH=./db/fixtures/test #フィクスチャーごとにデータ投入
FactoryBotの作成
テストのたびにbeforeとかにDBのレコードを作成して用意するのが嫌(というかソースも見づらくて気持ち悪い)なので、データの生成をスマートにするためにFactoryBotというツールがあります。元々FactoryGirlという名称でしたが、ジェンダーコンシャスの流れを受けて今の名前になりました。既にGemfileに追加してインストールしているので後はファイルをspec/factories配下に書いていくだけです。必要となる各モデルごとに作成していきます。
FactoryBot.define do
factory :user do
sequence(:name) { |n| "TEST_NAME#{n}"} # 名前のバリデーションにかからないもの
sequence(:email) { |n| "TEST#{n}@example.com"} # メールのバリデーションにかからないもの
password { 'password' } # パスワードのバリデーションにかからないもの
end
end
番号などシーケンシャルなものを挿入したい場合はsequenceというのを使います。
FactoryBot.define do
factory :review, class: Review do
restaurant_id {1620}
user_id {1}
rating {5}
title {'What a hell?'}
image {Rack::Test::UploadedFile.new(File.join(Rails.root, 'spec/fixtures/image.jpg'))}
comment {'It was yummy, indeed'}
end
end
画像データをカラムに入れている場合は上記のように画像のダミーデータをspec/fixtures配下に置いてRack::Test::UploadedFile.newとします。
リレーションがあるモデル(has_one/has_many/belongs_to)の場合、
class Menu < ActiveRecord::Base
has_many :menu_translations, dependent: :destroy
class MenuTranslation < ActiveRecord::Base
belongs_to :menu
以下のようにFactoryBotを作成します。
FactoryBot.define do
factory :menu, class: Menu do
restaurant_id {1}
menu_type {5}
image {Rack::Test::UploadedFile.new(File.join(Rails.root, 'spec/fixtures/image.jpg'))}
last_update_user_id {1000}
add_attribute(:public) {'1'}
association :restaurant
end
end
ちなみにカラム名がRSpecの予約語とかぶってしまうものを使っている場合(aliasとかpublicなど)、add_attribute(:public)という書き方をすることが出来ます。
FactoryBot.define do
factory :menu_translation_ja, class: MenuTranslation do
menu_id {}
menuname {'パルミジャーノとケールのイタリアン菜園サラダ'}
description {'SサイズとMサイズがあります。'}
price {1000}
currency {'円'}
lang {'ja'}
association :menu
end
end
associationでリレーションを定義します。
Modelのスペック
deviseをログイン認証機構に用いた場合のモデルのテストは以下の通りです。
deviseで注意すべきはモデル(今回だとUser)に書いてないバリデーションもあり、そちらもテストするのか、それ以外のカスタムバリデーションだけテストするのかを考えてコードを記載します。デフォルトではメールアドレスとパスワードの存在チェックはdeviseに組み込まれているようでした。
それ以外は普通にバリデーションやメソッドについてテストを記載します。
require 'rails_helper'
RSpec.describe User, type: :model do
let(:user) { build(:user)}
describe 'Check Validations' do
it 'default OK' do
expect(user.valid?).to eq(true)
end
it 'if no password, then NG' do
user.password = ''
expect(user.valid?).to eq(false)
expect(user.errors[:password]).to include 'パスワードを入力してください'
end
it 'if no name, then NG' do
user.name = ''
expect(user.valid?).to eq(false)
end
it 'if name exceed 120 letters, then NG' do
user.name = '1234567891234567891234567891234567891234567891234567891234567891234567891234567891234567891234567891234567891234567890000000000000123456789012345678901234567890'
user.completed_at = Time.current
expect(user.valid?).to eq(false)
end
end
end
こちらがModelの本体です。これをテストするためにSpecを記載しました。
class User < ApplicationRecord
mount_uploader :pic, ImageUploader
attr_accessor :current_password
validates :name,
length: { minimum: 1, maximum: 120 }, if: :is_registration?
validates :name, presence: true
end
FactoryBotには正常系を作成しておいて、バリデーションのテストの中で異常データを挿入してvalid?を試すイメージです。
Controllerのスペック
EverydayRails ‐ RSpecによるRailsテスト入門というRSpecの有名書籍によればコントローラーのテストはやがてなくなり、requestsのテストとして集約されるとのことです。いろいろな現場でテストを書かせてもらいましたが、contorollerにテストを書いているところは多いです。コントローラーにアクセスして、所定のキーワードがあるかどうか、関連するモデルの数が増えたり減ったりするかといったテストです。スマホアプリ構築の流れでWebAPIの開発というパターンも多いことからこれをテストするためにrequestsにテストを書くということのようですが、ここにControllerも集約されるぽいです。認識違っていたらすみません。
以下は弊社サービスVegewelでレストランガイドをやっておりまして、ページを表示、レビューの投稿をテストするコードを書いたものです。
require 'rails_helper'
RSpec.describe RestaurantController, type: :request do
describe "GET index" do
before do
@restaurants = FactoryBot.create(:restaurant)
end
it "responds successfully" do
get "/restaurant"
expect(response).to be_success
end
end
describe "GET show on restaurant detail" do
it "responds successfully" do
get "/restaurant/#{@restaurants.id}"
expect(response).to be_success
end
end
end
コントローラーのテストの基本の作りはシンプルです。
ルーティングに合わせてget, post, patch, put, delete...を発効して結果をexpectのtoに期待値を書いていくというものです。
require 'rails_helper'
RSpec.describe ReviewsController, type: :request do
describe "POST review" do
let(:review) { FactoryBot.create :review }
before do
@user = FactoryBot.create(:user)
@restaurant = FactoryBot.create(:restaurant)
@user.confirm # deviseのメール認証が必要なケースでは必須
sign_in @user
end
it "responds successfully" do
expect{
post '/reviews', params: { user_id: @user.id, restaurant_id: @restaurant.id, review: review }
}.to change(Review, :count).by(1)
end
end
end
post など更新がかかるアクションにはパラメータを設定しておきます。
Factoryの使い方としては事前評価と遅延評価という考え方があり、もともとAだったものがBになるということをテストするならベースとなるデータを事前評価ということでlet!でデータをcreateなどしておいて、更新の結果で増減があるか確かめるという流れです。
単純にレストランデータを作って該当URL(restaurant/1234みたいな)もので表示するだけなら遅延評価でletでデータを作成するので十分かと思います。
ちなみにフォームは確認画面とかはなく、登録・更新ボタン一発で変更が加わるようにしています。
追加したら当該モデルの数が+1、更新したら変更後の該当カラムが想定通りに変更されていることを確認するようなコードにしています。
リレーションのあるモデルを同時に更新する場合のコントローラーのSpecの書き方
コントローラーに以下のようなparamsの設定があるとします。
private
def menu_params
params.require(:menu).permit(:id, :restaurant_id , :image1, :image2, :image3, :last_update_user_id, :public, menu_type_list: [], menu_translations_attributes:[:id, :menu_id, :menuname, :description, :price, :currency, :lang])
end
パラメータに親子関係を設定したものとFactoryの設定にassciationを仕込ませておいた上で以下のようなテストコードを書きます。
# 新規作成
describe "POST /menu with authentication" do
before do
@admin_user = FactoryBot.create(:admin_user)
sign_in @admin_user
end
it "Create new menu for Japanese" do
menu_translation_params_ja = {
menu_translations_attributes: {
"0": FactoryBot.attributes_for(:menu_translation_ja)
}
}
menu_params = FactoryBot.attributes_for(:menu, restaurant_id: restaurant.id).merge(menu_translation_params_ja)
expect{
post '/menu', params: { menu: menu_params }
}.to change(Menu, :count).by(1)
end
# 更新
describe "PATCH /menu with authentication" do
let(:restaurant) { FactoryBot.create(:restaurant) }
let!(:menu) { FactoryBot.create(:menu, restaurant_id: restaurant.id) }
before do
@admin_user = FactoryBot.create(:admin_user)
sign_in @admin_user
end
it "Update menu for Japanese" do
menu_translation_params_ja = {
menu_translations_attributes: {
"0": FactoryBot.attributes_for(:menu_translation_ja, menuname: "ベジプレート(大豆ミートの鶏から風)", price: 500, menu_id: menu.id)
}
}
menu_params = FactoryBot.attributes_for(:menu, id: menu.id, restaurant_id: restaurant.id).merge(menu_translation_params_ja)
patch "/menu/#{menu.id}", params: {menu: menu_params}
expect(menu.menu_translations.first.menuname).to eq("ベジプレート(大豆ミートの鶏から風)")
expect(menu.menu_translations.first.price).to eq(500)
end
end
上記同様にフォームは確認画面とかはなく、登録・更新ボタン一発で変更が加わるようにしています。
追加したら当該モデルの数が+1、更新したら変更後の該当カラムが想定通りに変更されていることを確認するようなコードにしています。
以上でコントローラーのテストについて記載しました。
サイトの表側のふるまいをテストするフィーチャーテスト
Capybaraを用いてサイト上の動作をシミュレートしたテスト(フィーチャーテスト)をすることが出来ます。
RSpecをやるまではテストのコードを書くというとこっちのイメージでした。Seleniumはこちらのイメージと言えるでしょう。
試しにサイトのトップページからリンクをたどって別ページに飛んで、そこから検索するというスクリプトを書いてみます。
require 'rails_helper'
RSpec.feature "Projects", type: :feature do
scenario "Vegewel success scenario in English" do
# Vegewel TOP
visit '/en'
expect(page).to have_content "Vegewel restaurant guide"
expect(page).to have_content "Vegewel Style"
# Restaurant Page
click_link ("Restaurant")
expect(page).to have_content "Tasty & Healthy Restaurants"
expect(page).to have_content "VESPERA"
# Search Restaurant
fill_in('q[g][1][restaurant_search]', with: 'Cafe')
check('q[g][0][veganmenu_eq]')
click_button ("Search")
expect(page).to have_content "Cafe*teria HANIWA"
end
end
ちなみに
特定のページを開く:visit
リンクを踏む:click_link
ボタンを押す:click_button
チェックボックスを押す:check
テキストボックスに入力:fill_in
で制御できます。ボタン押したり、リンククリックしたりの場所の特定はIDなどのセレクタや、ラベルなどが表示されていればその文言で指定することが出来ます。可読性を上げるためにボタンのラベルに指定するとかもアリでしょう。
その他の動作に関してはこちらを参照して、色々シミュレートしてみることが出来るかと思います。
https://qiita.com/morrr/items/0e24251c049180218db4
ご確認ください。
RSpecのテスト
$ bundle exec rspec
で全ての(モデル、コントローラー、フィーチャー)のテストは実行されます。個別に実行したければファイルを指定すればOKです。テスト通ればSuccessと表示され、NGだとエラー内容が表示されます。
CircleCIにRSpecのテストを組み込み
もともとBitbucketの特定ブランチ(Staging、Master)にソースをコミットしたらデプロイする仕組みを構築していました。そこにRSpecのテストを追加してみました。以下.circleci/config.ymlです。
本当は全体はかなり長いのですが、RSpec組み込んだところだけフォーカスしています。
version: 2.1
orbs:
ruby: circleci/ruby@0.1.2
jobs:
build:
working_directory: ~/xxxx
parallelism: 1
docker:
- image: circleci/build-image:ubuntu-14.04-XXL-upstart-1189-5614f37
steps:
- checkout
- run:
(諸々Deployコマンド)
rspec:
parallelism: 3
docker:
- image: circleci/ruby:2.5.0-node-browsers
environment:
- BUNDLER_VERSION: 1.16.1
- RAILS_ENV: 'test'
- image: circleci/mysql:5.7
environment:
- MYSQL_ALLOW_EMPTY_PASSWORD: 'true'
- MYSQL_ROOT_HOST: '127.0.0.1'
steps:
- checkout
- restore_cache:
key: v1-bundle-{{ checksum "Gemfile.lock" }}
- run:
name: install dependencies
command: |
gem install bundler -v 2.0.2
bundle install --jobs=4 --retry=3 --path vendor/bundle
- save_cache:
key: v1-bundle-{{ checksum "Gemfile.lock" }}
paths:
- ~/circleci-demo-workflows/vendor/bundle
# Database setup
- run: mv ./config/database.yml.ci ./config/database.yml
- run:
name: Databasesetup
command: |
bundle exec rake db:create
bundle exec rake db:schema:load
bundle exec rake db:seed_fu
# run tests!
- run:
name: Run rspec
command: |
mkdir /tmp/test-results
TEST_FILES="$(circleci tests glob "spec/**/*_spec.rb" | \
circleci tests split --split-by=timings)"
bundle exec rspec \
--format progress \
--format RspecJunitFormatter \
--out /tmp/test-results/rspec.xml \
--format progress \
$TEST_FILES
# collect reports
- store_test_results:
path: /tmp/test-results
- store_artifacts:
path: /tmp/test-results
destination: test-results
workflows:
version: 2
build-n-deploy:
jobs:
- rspec:
filters:
branches: #Spec走らせるブランチを限定(これないとフィーチャーブランチのコミット&Pushで勝手にCircleCIが走ってしまう)
only:
- staging
- master
- build:
requires:
- rspec # rspecしてからDeployするように
filters:
branches:
only:
- staging
- master
こんな感じでJobを分割してRspecとしたところに
- テスト環境構築
- Seedデータを投入
- RSpecテスト実行
を実行しています。
workflowsのところに全体的な流れとして、先にRSpecを流してからOKならDeployを実行する流れにしています。
あと、これが最も重要なポイントですが、これを実行するためにCircleCIに課金しました。 1ヶ月$30 です。
複数Job実行にするのは無料プランでは出来なかったからです。この金額で25000ポイントもらえるので、各リソースの利用状況ごとにポイントが減らされる仕組みになっています。
弊社のサービスは1回のデプロイで300〜400ポイントくらい消費するので実行は慎重にやることにしています。ポイントが無くなってきたら、自動的に+$30してポイント追加になります。
課金するとJobの並行実行も可能です。複数の同時デプロイなどに使えるかと思うので使い方はご検討ください。
その他トラブル対応
特定のURLで200が返ってこなくてエラー
例えば以下のようなテストを書いたとして、エラーになって返ってこないケースです。
it "Login returns 200" do
get index_path
expect(response).to have_http_status "200"
end
エラーはこんな感じです。
Failure/Error: expect(response.status).to eq 200
expected: 200
got: 302
つまりリダイレクトしているってことですよね?調べたところapplication_controller.rbでdeviseログイン時やhttp->httpsリダイレクトで302をやらかしていました。また、Staging環境ではBasic認証かけたりとか色々やっていて、条件分岐に環境変数を使っていたことからtest環境へのケアが出来ていなかったのも要因でした。
上記エラーになったらapplication_controller.rbのbefore_actionを片っ端からコメントアウトして1個づつつけたり外したりして試してみるのが吉です。
アプリケーションFQDNがwww.example.comとなってしまう?bad URIも発生
テストに以下のようなコードを書いたところbad URIとなり、しかもアプリケーションはwww.example.comというFQDNになってました。
require 'rails_helper'
RSpec.describe "Restaurants", type: :request do
describe "GET /index" do
it "index responds successfully" do
get :index
expect(response).to be_success
end
end
end
以下エラー
URI::InvalidURIError:
bad URI(is not URI?): http://www.example.com:80index
これは get :index
を get '/'
とすることでテストが通りました。:indexとすることで文字列にindexを足してしまうようです。また、www.example.comはRailsでActionPackの中にTestのSessionを司る箇所があり、そこで DEFAULT_HOST='www.example.com'
と定義されていて、これがRSpec内でURLに指定がなければ自動的に設定されてしまうのが原因でした。ただ、通常のテストではwww.example.comが問題になることがなく「そういうもの」と考えて特に気にしなくて良い模様です。自分の場合はpryで何度止めてもこのFQDNがサーバ名として定義されているので焦りましたが、RSpecのメンターしてくれた先生に聞いたら気にしなくてOKと言われて安堵しました。
CircleCIでMySQLのDockerImage構築中にDBセットアップを始めようとしてエラーになる
この図で言うところの Container circleci/mysql5.7
のところが終わっていないのに、DBのセットアップが始まってしまうということがありました。そもそも当初CircleCIは課金していないので基本的に複数Jobを回すようにしておらず、以下のように環境構築、RSpec実行、AWSCLIの設定、ElasticBeanstalkのデプロイまでまとめてやろうとしていました。
その中でいうとimage: circleci/mysql:5.7のところが長い時間かかっているのですが、これと同期をとる方法がわかりませんでした。
version: 2
jobs:
build:
working_directory: ~/xxxx
parallelism: 1
shell: /bin/bash --login
environment:
CIRCLE_ARTIFACTS: /tmp/circleci-artifacts
CIRCLE_TEST_REPORTS: /tmp/circleci-test-results
docker:
- image: circleci/build-image:ubuntu-14.04-XXL-upstart-1189-5614f37
- image: circleci/ruby:2.5.0-node-browsers
environment:
- BUNDLER_VERSION: 1.16.1
- RAILS_ENV: 'test'
- image: circleci/mysql:5.7
environment:
- MYSQL_ALLOW_EMPTY_PASSWORD: 'true'
- MYSQL_ROOT_HOST: '127.0.0.1'
steps:
- run: mkdir -p $CIRCLE_ARTIFACTS $CIRCLE_TEST_REPORTS
- run:
working_directory: ~/xxxx
command: pip install pip==20.3.4
- run:
working_directory: ~/xxxx
command: pip install urllib3==1.26
- run:
working_directory: ~/xxxx
command: pip install awsebcli --upgrade --user
- checkout
- run:
name: install dependencies
command: |
gem install bundler -v 2.0.2
bundle install --jobs=4 --retry=3 --path vendor/bundle
# Database setup
- run: mv ./config/database.yml.ci ./config/database.yml
- run:
name: Databasesetup
command: |
bundle exec rake db:create
bundle exec rake db:schema:load
# run tests!
- run:
name: Run rspec
command: |
mkdir /tmp/test-results
TEST_FILES="$(circleci tests glob "spec/**/*_spec.rb" | \
circleci tests split --split-by=timings)"
bundle exec rspec \
--format progress \
--format RspecJunitFormatter \
--out /tmp/test-results/rspec.xml \
--format progress \
$TEST_FILES
# collect reports
- store_test_results:
path: /tmp/test-results
- store_artifacts:
path: /tmp/test-results
destination: test-results
- run:
name: Deploy
command: |
if [ "${CIRCLE_BRANCH}" == "master" ]; then
echo "Deploy production"
eb deploy Vegewel-production
else
echo "Deploy staging"
eb deploy vegewel-staging
fi
workflows:
version: 2
build-n-deploy:
jobs:
- build:
filters:
branches:
only:
- staging
- master
最終的には課金して複数JobでRSpecとデプロイという形にしましたが、CircleCIのサポートによればDockerizeを使って待機させることもできるそうです。こちらのドキュメントを参照ください。
https://circleci.com/docs/ja/2.0/databases/#dockerize-%E3%82%92%E4%BD%BF%E7%94%A8%E3%81%97%E3%81%9F%E4%BE%9D%E5%AD%98%E9%96%A2%E4%BF%82%E3%81%AE%E5%BE%85%E6%A9%9F
acts_as_taggable_onを使った箇所のテスト
動的なタグ生成のためにacts_as_taggable_onを使って開発しているところもあると思います。非常に便利で私もよく利用しています。
これのテストですが、自分は以下のようにしました。前提として料理の種類をCuisineとしていて、コードのID:1に対してCuisineTranslationの日本語(lang:ja)に”和食”、英語(lang:en)に”Japanese”のように2レコード設定しているようなデータ構造とお考えください。また、タグ用のテーブルはtags, taggingsの2つです。
class Cuisine < ActiveRecord::Base
ActsAsTaggableOn::Tagging.table_name = 'taggings'
ActsAsTaggableOn::Tag.table_name = 'tags'
has_many :cuisine_translations
end
class CuisineTranslation < ActiveRecord::Base
belongs_to :cuisine
belongs_to :restaurant
scope :with_lang , -> { where(lang: I18n.locale )}
end
これを扱うRestaurantモデルは
class Restaurant < ActiveRecord::Base
ActsAsTaggableOn::Tagging.table_name = 'taggings'
ActsAsTaggableOn::Tag.table_name = 'tags'
acts_as_taggable_on :stations, :lines, :cuisines
こんな感じです。Factoryの設定はTagに対して行います。
FactoryBot.define do
factory :cuisine_list, class: ActsAsTaggableOn::Tag do
name {Cuisine.first.id}
end
として、RestaurantのFactoryに以下のようにかけば動くはずです。
FactoryBot.define do
factory :restaurant, class: Restaurant do
station_list {[FactoryBot.build(:station_list)]}
line_list {[FactoryBot.build(:line_list)]}
cuisine_list {[FactoryBot.build(:cuisine_list)]}
最後に
かなり長い文章になってしまいました。こういうときは記事を分けるのがいいかなと思いつつ、大半は他のQiitaにも書いてそうなことなので付加価値出そうとしてボリューミーになってしまいました。多少なりともお役に立つ内容があれば幸いです。