ActiveModel::Validations

バリデーションライブラリRubyRailsActive RecordActiveModelWeb開発

GitHub概要

rails/rails

Ruby on Rails

スター57,276
ウォッチ2,311
フォーク21,912
作成日:2008年4月11日
言語:Ruby
ライセンス:MIT License

トピックス

activejobactiverecordframeworkhtmlmvcrailsruby

スター履歴

rails/rails Star History
データ取得日時: 2025/8/13 01:43

ライブラリ

ActiveModel::Validations

概要

ActiveModel::ValidationsはRuby on Railsフレームワークの中核となるバリデーション機能です。Active RecordとActive Modelで使用される包括的なバリデーションシステムで、10年以上にわたってRailsアプリケーションの標準的なバリデーション手法として広く採用されています。豊富な内蔵バリデーターと柔軟なカスタムバリデーション機能により、データの整合性とビジネスルールの実装を簡潔かつ宣言的に記述できます。オブジェクトレベルでのバリデーションとモデル層での責任分離を可能にし、堅牢なWebアプリケーション開発を支援します。

詳細

ActiveModel::Validations 7.1.5は2025年現在の最新版で、Rails 7.1フレームワークの一部として提供されています。Active Recordモデルに限らず、Plain Old Ruby Objects(PORO)でも使用可能な柔軟な設計により、ドメインモデルやService Objectでも活用できます。内蔵バリデーターには、presence、length、format、uniqueness、numerical、inclusion、exclusionなど網羅的な機能を提供。条件付きバリデーション、カスタムバリデーター、エラーハンドリング、国際化対応など、エンタープライズレベルのアプリケーション開発で必要な全ての機能を兼ね備えています。

主な特徴

  • 包括的な内蔵バリデーター: 一般的なバリデーションニーズをカバーする豊富なバリデーター
  • 柔軟なカスタムバリデーション: ビジネスロジックに特化したカスタムバリデーターの実装
  • 条件付きバリデーション: :if, :unless, :onオプションによる動的バリデーション制御
  • 詳細なエラーハンドリング: エラーオブジェクトによる包括的なエラー情報管理
  • 国際化(I18n)対応: エラーメッセージの多言語対応とカスタマイズ
  • Rails統合: Active RecordとActive Modelでのシームレスな統合

メリット・デメリット

メリット

  • Railsエコシステムでの標準的な地位と豊富な実績
  • 宣言的で読みやすいバリデーション記述
  • 包括的な内蔵バリデーターによる開発効率
  • 条件付きバリデーションによる複雑なビジネスルールの実装
  • 詳細なエラーハンドリングとi18n対応
  • Railsの他の機能(forms、JSON API等)との深い統合

デメリット

  • Rails/ActiveModelに依存するため単体での使用が困難
  • 複雑なバリデーションロジックでのパフォーマンス課題
  • 大量データの一括バリデーション時のメモリ使用量
  • Rails以外のRubyプロジェクトでの統合コスト
  • カスタムバリデーターの学習コストとテストの複雑さ
  • スキーマ変更時のバリデーション同期の手動管理

参考ページ

書き方の例

インストールと基本セットアップ

# Gemfile
gem 'rails', '~> 7.1.0'
# または ActiveModel のみを使用する場合
gem 'activemodel', '~> 7.1.0'

# Bundlerでインストール
bundle install

# Rails新規プロジェクト作成
rails new myapp
cd myapp

# Active Model単体での使用例
require 'active_model'

class User
  include ActiveModel::Validations
  include ActiveModel::AttributeAssignment
  
  attr_accessor :name, :email, :age
  
  def initialize(attributes = {})
    assign_attributes(attributes)
  end
end

基本的なバリデーション定義

# Active Recordモデルでの基本的なバリデーション
class User < ApplicationRecord
  # 必須項目バリデーション
  validates :name, presence: true
  validates :email, presence: true
  
  # 文字列長バリデーション
  validates :name, length: { minimum: 2, maximum: 50 }
  validates :password, length: { minimum: 8 }
  
  # 形式バリデーション
  validates :email, format: { 
    with: /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i,
    message: "有効なメールアドレスを入力してください"
  }
  
  # 数値バリデーション
  validates :age, numericality: { 
    greater_than: 0, 
    less_than: 150,
    only_integer: true
  }
  
  # 一意性バリデーション
  validates :email, uniqueness: { case_sensitive: false }
  validates :username, uniqueness: { scope: :organization_id }
  
  # 選択肢バリデーション
  validates :status, inclusion: { in: %w[active inactive pending] }
  validates :role, exclusion: { in: %w[admin super_admin] }
  
  # 確認フィールドバリデーション
  validates :password, confirmation: true
  validates :email, confirmation: true
end

# バリデーションの実行例
user = User.new(
  name: "田中太郎",
  email: "[email protected]",
  age: 30,
  password: "securepass123",
  password_confirmation: "securepass123"
)

# バリデーション実行
if user.valid?
  puts "バリデーション成功"
  user.save
else
  puts "バリデーションエラー:"
  user.errors.full_messages.each do |error|
    puts "- #{error}"
  end
end

# 個別属性のバリデーション
if user.invalid?(:email)
  puts "メールアドレスが無効です: #{user.errors[:email].join(', ')}"
end

# Plain Old Ruby Object (PORO) での使用
class Contact
  include ActiveModel::Validations
  include ActiveModel::AttributeAssignment
  
  attr_accessor :name, :email, :subject, :message
  
  validates :name, :email, presence: true
  validates :email, format: { with: URI::MailTo::EMAIL_REGEXP }
  validates :subject, length: { minimum: 5, maximum: 100 }
  validates :message, length: { minimum: 10, maximum: 1000 }
  
  def initialize(attributes = {})
    assign_attributes(attributes)
  end
  
  def submit
    return false unless valid?
    
    # メール送信処理
    ContactMailer.new_contact(self).deliver_now
    true
  end
end

# POROwithバリデーションの使用例
contact = Contact.new(
  name: "山田花子",
  email: "[email protected]",
  subject: "お問い合わせ",
  message: "サービスについてご質問があります。"
)

if contact.submit
  puts "お問い合わせが送信されました"
else
  puts "エラー: #{contact.errors.full_messages.join(', ')}"
end

条件付きバリデーションとカスタムバリデーター

class User < ApplicationRecord
  # 条件付きバリデーション - :if, :unless オプション
  validates :terms_accepted, acceptance: { message: "利用規約に同意してください" },
            if: :requires_terms_acceptance?
  
  validates :tax_id, presence: true, if: :company_user?
  validates :age, presence: true, unless: :skip_age_validation?
  
  # プロック条件
  validates :password, length: { minimum: 12 }, 
            if: ->(user) { user.role == 'admin' }
  
  # 複数条件
  validates :phone_number, presence: true,
            if: [:profile_complete?, :notification_enabled?]
  
  # コンテキスト指定バリデーション
  validates :terms_accepted, acceptance: true, on: :create
  validates :current_password, presence: true, on: :update
  validates :admin_approval, presence: true, on: :admin_review
  
  # カスタムバリデーター(インラインメソッド)
  validate :password_complexity
  validate :email_domain_allowed
  validate :unique_username_per_organization
  
  private
  
  def requires_terms_acceptance?
    new_record? && role != 'admin'
  end
  
  def company_user?
    user_type == 'company'
  end
  
  def skip_age_validation?
    admin? || age_verification_waived?
  end
  
  def profile_complete?
    [name, email, address].all?(&:present?)
  end
  
  def notification_enabled?
    notifications.present?
  end
  
  # カスタムバリデーションメソッド
  def password_complexity
    return unless password.present?
    
    errors.add(:password, "大文字を含む必要があります") unless password.match?(/[A-Z]/)
    errors.add(:password, "小文字を含む必要があります") unless password.match?(/[a-z]/)
    errors.add(:password, "数字を含む必要があります") unless password.match?(/\d/)
    errors.add(:password, "特殊文字を含む必要があります") unless password.match?(/[!@#$%^&*(),.?":{}|<>]/)
  end
  
  def email_domain_allowed
    return unless email.present?
    
    allowed_domains = ['company.com', 'partner.org']
    domain = email.split('@').last&.downcase
    
    unless allowed_domains.include?(domain)
      errors.add(:email, "許可されたドメインのメールアドレスを使用してください")
    end
  end
  
  def unique_username_per_organization
    return unless username.present? && organization_id.present?
    
    existing_user = User.where(
      username: username,
      organization_id: organization_id
    ).where.not(id: id).first
    
    if existing_user
      errors.add(:username, "この組織内で既に使用されているユーザー名です")
    end
  end
end

# カスタムバリデータークラス
class EmailValidator < ActiveModel::EachValidator
  def validate_each(record, attribute, value)
    return unless value.present?
    
    unless URI::MailTo::EMAIL_REGEXP.match?(value)
      record.errors.add(attribute, "有効なメールアドレス形式ではありません")
    end
    
    # 追加のメールドメイン検証
    if options[:allowed_domains]
      domain = value.split('@').last&.downcase
      unless options[:allowed_domains].include?(domain)
        record.errors.add(attribute, "許可されていないドメインです")
      end
    end
    
    # 使い捨てメールアドレスの検証
    if options[:block_disposable]
      disposable_domains = ['tempmail.org', '10minutemail.com', 'guerrillamail.com']
      domain = value.split('@').last&.downcase
      if disposable_domains.include?(domain)
        record.errors.add(attribute, "使い捨てメールアドレスは使用できません")
      end
    end
  end
end

class PhoneNumberValidator < ActiveModel::EachValidator
  def validate_each(record, attribute, value)
    return unless value.present?
    
    # 日本の電話番号形式をチェック
    phone_regex = /\A(\+81|0)[0-9\-\(\)\s]{8,14}\z/
    unless phone_regex.match?(value)
      record.errors.add(attribute, "有効な電話番号形式ではありません")
    end
  end
end

# カスタムバリデーターの使用
class Company < ApplicationRecord
  validates :contact_email, email: { 
    allowed_domains: ['company.com', 'business.org'],
    block_disposable: true
  }
  validates :phone, phone_number: true
end

# 複雑なビジネスルールバリデーション
class Order < ApplicationRecord
  validates :delivery_date, presence: true
  validate :delivery_date_must_be_future
  validate :delivery_date_not_on_weekend
  validate :sufficient_inventory
  validate :payment_method_valid
  
  private
  
  def delivery_date_must_be_future
    return unless delivery_date.present?
    
    if delivery_date <= Date.current
      errors.add(:delivery_date, "配送日は今日以降の日付を選択してください")
    end
  end
  
  def delivery_date_not_on_weekend
    return unless delivery_date.present?
    
    if delivery_date.saturday? || delivery_date.sunday?
      errors.add(:delivery_date, "配送日は平日を選択してください")
    end
  end
  
  def sufficient_inventory
    return unless items.present?
    
    items.each do |item|
      if item.quantity > item.product.stock_quantity
        errors.add(:items, "#{item.product.name}の在庫が不足しています")
      end
    end
  end
  
  def payment_method_valid
    return unless payment_method.present?
    
    case payment_method
    when 'credit_card'
      errors.add(:payment_method, "クレジットカード情報が無効です") unless valid_credit_card?
    when 'bank_transfer'
      errors.add(:payment_method, "銀行口座情報が無効です") unless valid_bank_account?
    end
  end
  
  def valid_credit_card?
    # クレジットカード検証ロジック
    credit_card_number.present? && credit_card_number.match?(/\A\d{13,19}\z/)
  end
  
  def valid_bank_account?
    # 銀行口座検証ロジック
    bank_account_number.present? && bank_account_number.match?(/\A\d{7,8}\z/)
  end
end

エラーハンドリングとメッセージカスタマイズ

# エラーオブジェクトの詳細操作
class Product < ApplicationRecord
  validates :name, presence: true, length: { minimum: 2, maximum: 100 }
  validates :price, numericality: { greater_than: 0 }
  validates :category, inclusion: { in: %w[electronics clothing books food] }
end

product = Product.new(name: "", price: -100, category: "invalid")

# バリデーション実行
unless product.valid?
  puts "=== エラー詳細 ==="
  
  # 全てのエラーメッセージ
  puts "全エラー: #{product.errors.full_messages}"
  
  # 属性別エラー
  product.errors.each do |error|
    puts "属性: #{error.attribute}, メッセージ: #{error.message}, 値: #{error.options[:value]}"
  end
  
  # 特定属性のエラー確認
  if product.errors[:name].any?
    puts "名前エラー: #{product.errors[:name].join(', ')}"
  end
  
  # エラー数
  puts "エラー数: #{product.errors.count}"
  
  # エラーの詳細情報
  product.errors.details.each do |attribute, details|
    puts "#{attribute}: #{details}"
  end
end

# カスタムエラーメッセージ
class User < ApplicationRecord
  validates :name, presence: { message: "名前を入力してください" }
  validates :email, uniqueness: { message: "このメールアドレスは既に使用されています" }
  validates :age, numericality: { 
    greater_than: 18,
    message: "年齢は18歳以上である必要があります"
  }
  
  # プロック形式でのダイナミックメッセージ
  validates :username, length: { 
    minimum: 3,
    message: ->(object, data) do
      "ユーザー名は#{data[:count]}文字以上入力してください(現在#{object.username&.length || 0}文字)"
    end
  }
end

# 国際化(I18n)設定
# config/locales/ja.yml
ja:
  activerecord:
    models:
      user: "ユーザー"
      product: "商品"
    attributes:
      user:
        name: "名前"
        email: "メールアドレス"
        age: "年齢"
      product:
        name: "商品名"
        price: "価格"
    errors:
      models:
        user:
          attributes:
            email:
              taken: "既に使用されているメールアドレスです"
              invalid: "有効なメールアドレスを入力してください"
      messages:
        blank: "を入力してください"
        too_short: "は%{count}文字以上で入力してください"
        too_long: "は%{count}文字以下で入力してください"
        not_a_number: "は数値で入力してください"

# エラーハンドリングユーティリティクラス
class ValidationErrorHandler
  def self.format_errors(model)
    return {} unless model.errors.any?
    
    formatted_errors = {}
    
    model.errors.each do |error|
      attribute = error.attribute
      formatted_errors[attribute] ||= []
      formatted_errors[attribute] << {
        message: error.message,
        type: error.type,
        options: error.options
      }
    end
    
    formatted_errors
  end
  
  def self.errors_to_json(model)
    {
      success: false,
      errors: format_errors(model),
      error_count: model.errors.count
    }.to_json
  end
  
  def self.first_error_message(model)
    model.errors.full_messages.first
  end
  
  def self.errors_for_attribute(model, attribute)
    model.errors[attribute].map do |message|
      {
        attribute: attribute,
        message: message
      }
    end
  end
end

# API応答での使用例
class UsersController < ApplicationController
  def create
    @user = User.new(user_params)
    
    if @user.save
      render json: {
        success: true,
        data: @user,
        message: "ユーザーが正常に作成されました"
      }, status: :created
    else
      render json: ValidationErrorHandler.errors_to_json(@user), 
             status: :unprocessable_entity
    end
  end
  
  private
  
  def user_params
    params.require(:user).permit(:name, :email, :password, :password_confirmation)
  end
end

# フォームでのエラー表示
# app/views/users/_form.html.erb
<%= form_with(model: user, local: true) do |form| %>
  <% if user.errors.any? %>
    <div id="error_explanation" class="alert alert-danger">
      <h4><%= pluralize(user.errors.count, "error") %> prohibited this user from being saved:</h4>
      <ul>
        <% user.errors.full_messages.each do |message| %>
          <li><%= message %></li>
        <% end %>
      </ul>
    </div>
  <% end %>

  <div class="field">
    <%= form.label :name, "名前" %>
    <%= form.text_field :name, class: "form-control #{'is-invalid' if user.errors[:name].any?}" %>
    <% if user.errors[:name].any? %>
      <div class="invalid-feedback">
        <%= user.errors[:name].first %>
      </div>
    <% end %>
  </div>

  <div class="field">
    <%= form.label :email, "メールアドレス" %>
    <%= form.email_field :email, class: "form-control #{'is-invalid' if user.errors[:email].any?}" %>
    <% if user.errors[:email].any? %>
      <div class="invalid-feedback">
        <%= user.errors[:email].join(', ') %>
      </div>
    <% end %>
  </div>
<% end %>

Service Objectsとフォームオブジェクトでの活用

# Service Object パターン
class UserRegistrationService
  include ActiveModel::Validations
  include ActiveModel::AttributeAssignment
  
  attr_accessor :name, :email, :password, :password_confirmation, 
                :terms_accepted, :newsletter_subscription
  
  validates :name, presence: true, length: { minimum: 2 }
  validates :email, presence: true, format: { with: URI::MailTo::EMAIL_REGEXP }
  validates :password, presence: true, length: { minimum: 8 }
  validates :password_confirmation, presence: true
  validates :terms_accepted, acceptance: true
  
  validate :passwords_match
  validate :email_not_taken
  
  def initialize(params = {})
    assign_attributes(params)
  end
  
  def call
    return false unless valid?
    
    ActiveRecord::Base.transaction do
      user = create_user
      send_welcome_email(user)
      subscribe_to_newsletter(user) if newsletter_subscription
      user
    end
  rescue => e
    errors.add(:base, "登録処理中にエラーが発生しました: #{e.message}")
    false
  end
  
  private
  
  def passwords_match
    return unless password.present? && password_confirmation.present?
    
    unless password == password_confirmation
      errors.add(:password_confirmation, "パスワードが一致しません")
    end
  end
  
  def email_not_taken
    return unless email.present?
    
    if User.exists?(email: email)
      errors.add(:email, "このメールアドレスは既に使用されています")
    end
  end
  
  def create_user
    User.create!(
      name: name,
      email: email,
      password: password
    )
  end
  
  def send_welcome_email(user)
    UserMailer.welcome(user).deliver_now
  end
  
  def subscribe_to_newsletter(user)
    NewsletterSubscriptionService.new(user).subscribe
  end
end

# Form Object パターン
class ContactForm
  include ActiveModel::Model
  include ActiveModel::Attributes
  include ActiveModel::Validations
  
  attribute :name, :string
  attribute :email, :string
  attribute :subject, :string
  attribute :message, :string
  attribute :category, :string
  
  validates :name, presence: true, length: { maximum: 100 }
  validates :email, presence: true, format: { with: URI::MailTo::EMAIL_REGEXP }
  validates :subject, presence: true, length: { minimum: 5, maximum: 200 }
  validates :message, presence: true, length: { minimum: 10, maximum: 2000 }
  validates :category, inclusion: { in: %w[general support sales technical] }
  
  def submit
    return false unless valid?
    
    send_contact_email
    save_contact_record
    true
  end
  
  def persisted?
    false
  end
  
  private
  
  def send_contact_email
    ContactMailer.new_inquiry(form_attributes).deliver_now
  end
  
  def save_contact_record
    Contact.create!(form_attributes)
  end
  
  def form_attributes
    {
      name: name,
      email: email,
      subject: subject,
      message: message,
      category: category
    }
  end
end

# 複雑なビジネスルール用 Service Object
class OrderProcessingService
  include ActiveModel::Validations
  include ActiveModel::AttributeAssignment
  
  attr_accessor :user, :items, :shipping_address, :payment_method, 
                :coupon_code, :delivery_date
  
  validates :user, presence: true
  validates :items, presence: true
  validates :shipping_address, presence: true
  validates :payment_method, inclusion: { in: %w[credit_card paypal bank_transfer] }
  
  validate :items_valid
  validate :shipping_address_valid
  validate :payment_method_valid
  validate :coupon_valid
  validate :delivery_date_valid
  validate :sufficient_inventory
  validate :user_can_order
  
  def initialize(params = {})
    assign_attributes(params)
  end
  
  def process
    return false unless valid?
    
    ActiveRecord::Base.transaction do
      order = create_order
      process_payment(order)
      update_inventory
      send_confirmation_email(order)
      order
    end
  rescue => e
    errors.add(:base, "注文処理中にエラーが発生しました: #{e.message}")
    false
  end
  
  private
  
  def items_valid
    return unless items.present?
    
    items.each_with_index do |item, index|
      unless item[:product_id].present?
        errors.add(:items, "#{index + 1}番目の商品IDが指定されていません")
      end
      
      unless item[:quantity].present? && item[:quantity] > 0
        errors.add(:items, "#{index + 1}番目の商品の数量が無効です")
      end
    end
  end
  
  def shipping_address_valid
    return unless shipping_address.present?
    
    required_fields = %w[name postal_code prefecture city address]
    required_fields.each do |field|
      unless shipping_address[field].present?
        errors.add(:shipping_address, "#{field}が入力されていません")
      end
    end
  end
  
  def payment_method_valid
    return unless payment_method.present?
    
    case payment_method
    when 'credit_card'
      validate_credit_card
    when 'paypal'
      validate_paypal_account
    when 'bank_transfer'
      validate_bank_account
    end
  end
  
  def coupon_valid
    return unless coupon_code.present?
    
    coupon = Coupon.find_by(code: coupon_code)
    unless coupon&.valid_for_user?(user)
      errors.add(:coupon_code, "無効なクーポンコードです")
    end
  end
  
  def delivery_date_valid
    return unless delivery_date.present?
    
    if delivery_date <= Date.current
      errors.add(:delivery_date, "配送日は今日以降を指定してください")
    end
    
    if delivery_date.saturday? || delivery_date.sunday?
      errors.add(:delivery_date, "配送日は平日を指定してください")
    end
  end
  
  def sufficient_inventory
    return unless items.present?
    
    items.each do |item|
      product = Product.find(item[:product_id])
      if product.stock_quantity < item[:quantity]
        errors.add(:items, "#{product.name}の在庫が不足しています")
      end
    end
  end
  
  def user_can_order
    return unless user.present?
    
    if user.suspended?
      errors.add(:user, "アカウントが停止されているため注文できません")
    end
    
    if user.unpaid_orders.count >= 3
      errors.add(:user, "未払いの注文があるため新規注文できません")
    end
  end
  
  def validate_credit_card
    # クレジットカード検証ロジック
  end
  
  def validate_paypal_account
    # PayPal検証ロジック
  end
  
  def validate_bank_account
    # 銀行口座検証ロジック
  end
  
  def create_order
    # 注文作成ロジック
  end
  
  def process_payment(order)
    # 決済処理ロジック
  end
  
  def update_inventory
    # 在庫更新ロジック
  end
  
  def send_confirmation_email(order)
    # 確認メール送信ロジック
  end
end

# 使用例
registration_service = UserRegistrationService.new(
  name: "田中太郎",
  email: "[email protected]",
  password: "securepass123",
  password_confirmation: "securepass123",
  terms_accepted: "1",
  newsletter_subscription: true
)

if registration_service.call
  puts "ユーザー登録が完了しました"
else
  puts "登録エラー:"
  registration_service.errors.full_messages.each do |error|
    puts "- #{error}"
  end
end

# フォームオブジェクトの使用例(Controllerから)
class ContactsController < ApplicationController
  def new
    @contact_form = ContactForm.new
  end
  
  def create
    @contact_form = ContactForm.new(contact_params)
    
    if @contact_form.submit
      redirect_to root_path, notice: "お問い合わせを送信しました"
    else
      render :new, status: :unprocessable_entity
    end
  end
  
  private
  
  def contact_params
    params.require(:contact_form).permit(:name, :email, :subject, :message, :category)
  end
end

テストでのバリデーション検証

# RSpec でのバリデーションテスト
RSpec.describe User, type: :model do
  describe "validations" do
    subject { build(:user) }
    
    # 必須項目テスト
    it { should validate_presence_of(:name) }
    it { should validate_presence_of(:email) }
    
    # 文字列長テスト
    it { should validate_length_of(:name).is_at_least(2).is_at_most(50) }
    it { should validate_length_of(:password).is_at_least(8) }
    
    # 形式テスト
    it { should allow_value("[email protected]").for(:email) }
    it { should_not allow_value("invalid_email").for(:email) }
    
    # 一意性テスト
    it { should validate_uniqueness_of(:email).case_insensitive }
    
    # 数値テスト
    it { should validate_numericality_of(:age).is_greater_than(0).is_less_than(150) }
    
    # 選択肢テスト
    it { should validate_inclusion_of(:status).in_array(%w[active inactive pending]) }
    
    # カスタムバリデーションテスト
    describe "#password_complexity" do
      it "requires uppercase letter" do
        user = build(:user, password: "lowercase123!")
        expect(user).not_to be_valid
        expect(user.errors[:password]).to include("大文字を含む必要があります")
      end
      
      it "requires lowercase letter" do
        user = build(:user, password: "UPPERCASE123!")
        expect(user).not_to be_valid
        expect(user.errors[:password]).to include("小文字を含む必要があります")
      end
      
      it "requires number" do
        user = build(:user, password: "Password!")
        expect(user).not_to be_valid
        expect(user.errors[:password]).to include("数字を含む必要があります")
      end
      
      it "requires special character" do
        user = build(:user, password: "Password123")
        expect(user).not_to be_valid
        expect(user.errors[:password]).to include("特殊文字を含む必要があります")
      end
      
      it "accepts valid complex password" do
        user = build(:user, password: "ValidPass123!")
        expect(user).to be_valid
      end
    end
    
    # 条件付きバリデーションテスト
    describe "conditional validations" do
      context "when user is company type" do
        it "requires tax_id" do
          user = build(:user, user_type: "company", tax_id: nil)
          expect(user).not_to be_valid
          expect(user.errors[:tax_id]).to include("can't be blank")
        end
      end
      
      context "when user is individual type" do
        it "does not require tax_id" do
          user = build(:user, user_type: "individual", tax_id: nil)
          expect(user).to be_valid
        end
      end
    end
  end
  
  # エラーメッセージテスト
  describe "error messages" do
    it "returns custom error message for invalid email" do
      user = build(:user, email: "invalid")
      user.valid?
      expect(user.errors[:email]).to include("有効なメールアドレスを入力してください")
    end
  end
  
  # バリデーションコンテキストテスト
  describe "validation contexts" do
    let(:user) { create(:user) }
    
    it "validates current_password only on update context" do
      expect(user).to be_valid
      expect(user).not_to be_valid(:update)
      expect(user.errors[:current_password]).to include("can't be blank")
    end
  end
end

# Minitest でのバリデーションテスト
class UserTest < ActiveSupport::TestCase
  def setup
    @user = User.new(
      name: "テストユーザー",
      email: "[email protected]",
      password: "Password123!"
    )
  end
  
  test "should be valid with valid attributes" do
    assert @user.valid?
  end
  
  test "should require name" do
    @user.name = nil
    assert_not @user.valid?
    assert_includes @user.errors[:name], "can't be blank"
  end
  
  test "should require valid email format" do
    invalid_emails = %w[invalid @example.com test@ [email protected]]
    
    invalid_emails.each do |email|
      @user.email = email
      assert_not @user.valid?, "#{email} should be invalid"
      assert_includes @user.errors[:email], "有効なメールアドレスを入力してください"
    end
  end
  
  test "should enforce unique email" do
    duplicate_user = @user.dup
    @user.save!
    
    assert_not duplicate_user.valid?
    assert_includes duplicate_user.errors[:email], "has already been taken"
  end
  
  test "password should meet complexity requirements" do
    weak_passwords = ["password", "PASSWORD", "12345678", "Pass123"]
    
    weak_passwords.each do |password|
      @user.password = password
      assert_not @user.valid?, "#{password} should be invalid"
    end
  end
end

# Factory Bot でのテストデータ作成
FactoryBot.define do
  factory :user do
    name { "テストユーザー" }
    sequence(:email) { |n| "user#{n}@example.com" }
    password { "ValidPass123!" }
    password_confirmation { password }
    age { 25 }
    status { "active" }
    
    trait :admin do
      role { "admin" }
    end
    
    trait :company do
      user_type { "company" }
      tax_id { "1234567890" }
    end
    
    trait :invalid do
      email { "invalid_email" }
    end
  end
end

# バリデーションテスト用のヘルパーメソッド
module ValidationTestHelpers
  def assert_required_field(model, field)
    model.send("#{field}=", nil)
    assert_not model.valid?
    assert_includes model.errors[field], "can't be blank"
  end
  
  def assert_field_length(model, field, min: nil, max: nil)
    if min
      model.send("#{field}=", "a" * (min - 1))
      assert_not model.valid?
      assert_includes model.errors[field], "is too short"
    end
    
    if max
      model.send("#{field}=", "a" * (max + 1))
      assert_not model.valid?
      assert_includes model.errors[field], "is too long"
    end
  end
  
  def assert_valid_formats(model, field, valid_values)
    valid_values.each do |value|
      model.send("#{field}=", value)
      assert model.valid?, "#{value} should be valid for #{field}"
    end
  end
  
  def assert_invalid_formats(model, field, invalid_values)
    invalid_values.each do |value|
      model.send("#{field}=", value)
      assert_not model.valid?, "#{value} should be invalid for #{field}"
    end
  end
end