はじめに
Ralisを使って実装中、テストコード実行時にMySQLエラーが発生しましたので、原因と解決策を記載しておきます。
同じ原因でつまづいてしまった方がいた時のお力になれればと思います。
なお、DB内の処理について、深い知見があるほどの技術力ではございません。それ故に、エラーの原因など、「厳密には違う」ということがあるかもしれません。
ご指摘いただければ修正いたしますので、どうか温かい目でご覧ください。
開発環境と事前状況
バージョン
・ruby 2.6.5
・rails 6.0.3.4
・mysql 14.14
事前状況
・商品の購入を行うための、ItemBuyモデルを構築済み
class ItemBuy
include ActiveModel::Model
attr_accessor :postal_code, :from_id, :municipality, :house_number, :building_name, :tell, :user_id, :item_id, :token
with_options presence: true do
validates :token
validates :postal_code, format: { with: /\A[0-9]{3}-[0-9]{4}\z/, message: 'Input correctly' }
validates :from_id, numericality: { other_than: 1, message: 'Select' }
validates :municipality
validates :house_number
validates :tell, format: { with: /\A\d{10,11}\z/, message: 'Input only number' }
validates :user_id
validates :item_id
end
#中略
end
・テスト環境として、Rspec、FactoryBot、Fakerを導入済み
gem 'rspec-rails', '~> 4.0.0'
gem 'factory_bot_rails'
gem 'faker'
# bundle install実行済み
require 'rails_helper'
RSpec.describe ItemBuy, type: :model do
before do
user = FactoryBot.create(:user)
item = FactoryBot.create(:item)
@item_buy = FactoryBot.build(:item_buy, user_id: user.id, item_id: item.id)
end
describe '商品購入' do
context '商品購入がうまくいく時' do
it '必須項目が全て入力されていれば購入できる' do
expect(@item_buy).to be_valid
end
it '建物名が空でも購入できる' do
@item_buy.building_name = ''
expect(@item_buy).to be_valid
end
end
context '商品購入がうまくいかない時' do
it '郵便番号が空では購入ができない' do
@item_buy.postal_code = ''
@item_buy.valid?
expect(@item_buy.errors.full_messages).to include("Postal code can't be blank")
end
it '郵便番号にハイフンがなければ購入ができない' do
@item_buy.postal_code = '1111111'
@item_buy.valid?
expect(@item_buy.errors.full_messages).to include('Postal code Input correctly')
end
# 中略(実際にはこの他に10個の異常系itがありますが長くなるので削ります)
end
end
end
FactoryBot.define do
factory :item_buy do
postal_code { '123-4567' }
from_id { 2 }
municipality { '相模原市' }
house_number { '大山1-1-1' }
building_name { 'tower'}
tell { '09012345678' }
token { 'tok_abcdefghijk00000000000000000' }
end
end
以上の状況下でbundle exec rspec spec/models/item_buy_spec.rb
をターミナルで実行。
すると、下記のエラーが発生。テストはうまくいかなかった模様。
仮説
さて、エラー文を読むと、、、
「Mysql」エラーが発生しているようでした。
DBとのデータのやり取りの際に何か起きているのかな・・・とこの時は思いましたが、itを一つ追加するごとにテストを実行し、確認しながら実装していたので、itの中身自体に問題があるわけではなさそうでした。
他に考えられることとしては、商品情報と購入者情報もテストするために、item_buy_spec.rb
内のbefore do
の中に以下の二文を追加していました。
user = FactoryBot.create(:user)
item = FactoryBot.create(:item)
ここが原因になっているのかなとも推察できました。
原因
結論、仮説の通りbefore do
の中の二文が原因でした。
before do
による処理はitの中身が読み込まれる前に実行されます。元々定義されていたのは、
@item_buy = FactoryBot.build(:item_buy, user_id: user.id, item_id: item.id)
この一つだけだったので、今まで問題はなかったのですが、user,itemという二つの変数を用意したことにより、
user = FactoryBot.create(:user)
item = FactoryBot.create(:item)
@item_buy = FactoryBot.build(:item_buy, user_id: user.id, item_id: item.id)
合計3回分の処理を事前に行わなければならなくなりました。
つまり、「userのFactoryBotをcreateして、itemのFactoryBotをcreateして、、、」としている間にテストコードのitを読みに行ってしまい、DBとのやりとりが間に合わなくなって、Mysqlエラーが発生した、という状況が起きていたようです。
解決策
この問題は、sleepメソッドを使うことで解決できました。
sleepメソッドとは、指定した時間、処理を停止することのできるrubyのメソッドの一つです。
例えばsleep(1)とすれば処理を1秒停止させられます。処理をとめて、データのやりとりを間に合うようにしようということです。
このsleepを0.1秒くらいの時間でbefore do
の処理の直後に埋め込んで・・・
RSpec.describe ItemBuy, type: :model do
before do
user = FactoryBot.create(:user)
item = FactoryBot.create(:item)
@item_buy = FactoryBot.build(:item_buy, user_id: user.id, item_id: item.id)
sleep(0.1)
end
再びbundle exec rspec spec/models/item_buy_spec.rb
実行!
今度はうまくいきました!!
このエラーを通じて感じたこと
テストコードに限らず、PCの処理速度は超速で、エラーの原因が「処理のやりとりに伴う速度」というのは今まであまり実感が湧いておらず、大体エラーが起きた時は自分のミスであることがほとんどで、コンマ〇〇秒の処理が問題になっているということはイメージもしていませんでした。
スペルミス、構文ミスなど、明らかなミスとは違って、このエラーは「実行した指示の中で今何が起きているのか」をちゃんと把握する重要性を感じさせてくれました。