blog.kotamiyake.me

為せば成る、為さねば成らぬ何事も

Railsはデフォルトで基本的なバリデーション機能を提供してくれています。

ただ提供されていないバリデーションを追加したいこともあります。そんなとき、一度しか登場しない機能であれば validate などで直にバリデーションを書いてしまいます。

ただ複数箇所で同じようなバリデーションを使いまわしたい場合があります。そこで登場するのが Custom Validators です。

Custom Validator の実装方法は Rails Guides を読めばわかるので割愛しますが、今回はそのテスト方法を紹介したいと思います。

今回する方法は以下の環境で試していますが、それほど古い環境でなければ同じように動作するかと思います。

  • ruby 2.5.3p105
  • Rails 5.2.2
  • RSpec 3.8

単純に実際に使用しているモデルをテストすればいいかと思うかもしれませんが、それではモデルと密に繋がってしまい、Custom Validator のテストがモデルに依存してしまいます。

そこで今回はバリデーションを定義したモックとなるモデルを生成してテストします。

まずはテスト対象となる Custom Validator を作成します。バリデーションの要件は「全角カタカナ」のみを含む、とします。

全角カタカナのチェックは 全角カタカナにのみマッチする正規表現 – Qiita を参考にしました。

class KatakanaValidator < ActiveModel::EachValidator
  def validate_each(record, attr_name, value)
    unless value =~ /\A[\p{katakana} ー-&&[^ -~。-゚]]+\z/i
      record.errors.add(attr_name, :katakana)
    end
  end
end

テストは 【Rails】まだValidatorのテストで消耗してるの? – Qiita を参考にしています。

上記の筆者の方がgemを作成していただいているのですが、それほど規模が大きくないこと、メンテナンスの継続性なども含めて自身で実装することにしました。

RSpecの使い方などは理解しているという前提で話を進めさせていただきます。そのあたりはいろいろな記事で紹介されているかと思いますので、そちらをご参照ください。

複数のテストで使うことからsupport/以下に共通モジュールとしてモック生成モジュールを作成します。

module CustomValidatorHelper
  def build_mock(attr_name , validator: )
    raise ArgumentError if attr_name.blank? || validator.blank?

    Struct.new(attr_name) do
      include ActiveModel::Validations

      def self.name
        'DummyModel'
      end

      validates attr_name, validator => true
    end
  end
end

RSpec.configure do |config|
  config.include CustomValidatorHelper, type: :model
end

この記事を書いていて、メソッドの名前は考え直したほうが良いなと思いました…が、とりあえずそこはスルーして読み進めてください。

それでは実際のテストコードです。

require 'rails_helper'

RSpec.describe KatakanaValidator, type: :model do
  describe '#validate_each' do
    it 'is valid' do
      mock_model = build_mock(:name_kana, validator: :katakana).new('カタカナ')
      expect(mock_model).to be_valid
    end

    it 'is not valid' do
      mock_model = build_mock(:name_kana, validator: :katakana).new('カタカナa')
      expect(mock_model).not_to be_valid
      expect(mock_model.errors).to be_added(:name_kana, :katakana)
    end
  end
end

これでモデルに依存せずに Custom Validator のテストを書けるようになりました。

テストはできるだけ依存を減らして、単体で機能するようにすると影響を受けずに安定したテストになるかと思います。

なのでそういったことも意識してテストを書くようにすると安定したサービスを開発できるのではないでしょうか。

また上記のコードは色々とリファクタリングできる要素があるかと思いますが、テストはあまりDRYを意識するよりは愚直な実装のほうが後から見た時にわかりやすくて良いかなと思うので、その辺のバランスも考えてテストコードを書いていくと、よりメンテナンスしやすいテストになりそうです。

以上、Rails の Custom Validator をテストする方法を紹介しました。

前回の記事で開発予定に挙げていた「複数の担当者を一括で設定できる」機能を実装しました!

今までは毎回一人ずつしか担当者を設定できなかったのですが、今回の実装で複数担当者を一括で設定できるようになりました。

今回はselect2を使って実装しました。

簡単に絞り込み可能なセレクトボックスが生成できるのでとても便利です。

ドキュメントも充実しているので、ぜひ皆さんのプロジェクトでも使ってみてください!

今日は個人プロジェクトで開発しているプロジェクト管理ツール「Seamless」の機能を紹介したいと思います。

「Seamless」ではタスク管理で必要な基本機能を備えています。

  • タスクリストの操作
  • タスクリストへのタスクの追加、削除
  • タスクの担当者設定(複数担当者を設定可能)
  • タスクの期限設定
  • タスク担当者への通知

まだまだ機能としては貧弱ですが、今後着実に機能開発、改善を進めていくのでぜひお試しください!

開発予定のタスクリスト新機能

  • 通知したいメンバーを選択できる
  • 複数の担当者を一括で設定できる
  • タスクを別のタスクリストに移動できる

注:必ず追加されるわけではありません…

個人で開発をすると、やはりリソースは限られているわけで、やはりそこはやらないことを決めていかないといけないわけで。

何に時間を掛けて何に時間を掛けないのかという見極めをする能力を身に着けていかないといけないと思う今日このごろなのでした。

v-bind:class を使えば良い。

ex.

<div v-bind:class="{ active: isActive }"></div>

Vue.jsのv-bind:classで動的なクラス割り当て – 親バカエンジニアのナレッジ帳
https://ti-tomo-knowledge.hatenablog.com/entry/2018/06/21/111721

以下のコードをinitializersあたりに設置して、その定義の下にwhitelistを追加する。

# from https://github.com/alexspeller/non-stupid-digest-assets/issues/48#issuecomment-365126225
module NonStupidDigestAssets
  mattr_accessor :whitelist
  @@whitelist = []

  class << self
    def files(files)
      return files if whitelist.empty?
      whitelisted_files(files)
    end

    private

    def whitelisted_files(files)
      files.select do |file, info|
        whitelist.any? do |item|
          case item
          when Regexp
            info['logical_path'] =~ item
          else
            info['logical_path'] == item
          end
        end
      end
    end
  end
end

module NonDigest
  def compile *args
    super *args
    
    NonStupidDigestAssets.files(files).each do |(digest_path, info)|
      full_digest_path = File.join dir, digest_path
      full_digest_gz_path = "#{full_digest_path}.gz"
      full_non_digest_path = File.join dir, info['logical_path']
      full_non_digest_gz_path = "#{full_non_digest_path}.gz"

      if File.exists? full_digest_path
        # logger.info "Writing #{full_non_digest_path}"
        FileUtils.rm full_non_digest_path if File.exists? full_non_digest_path
        FileUtils.cp full_digest_path, full_non_digest_path
      else
        logger.warn "Could not find: #{full_digest_path}"
      end
      if File.exists? full_digest_gz_path
        # logger.info "Writing #{full_non_digest_gz_path}"
        FileUtils.rm full_non_digest_gz_path if File.exists? full_non_digest_gz_path
        FileUtils.cp full_digest_gz_path, full_non_digest_gz_path
      else
        logger.warn "Could not find: #{full_digest_gz_path}"
      end
    end
  end
end

module Sprockets
  class Manifest
    prepend NonDigest
  end
end