Tech Racho エンジニアの「?」を「!」に。
  • Ruby / Rails関連

Rails: minitestでキャッシュをテストする

Rails 7で低レベルキャッシュを導入しました。Patternモデルのレコード数か最新レコードのタイムスタンプが変わらなければ、Pattern#fetch_regexメソッドでキャッシュが効くようにしました。

class Pattern
  # クエリ結果をキャッシュする
  def fetch_regex
    latest_regex = Pattern.order(:updated_at).last.updated_at.to_formatted_s(:number)
    Rails.cache.fetch("/model/pattern/check/#{Pattern.count}_#{latest_regex}", expires_in: 1.day, compress: true) do
      Pattern.pluck(*KEYS).map do |line|
        line[1] = Regexp.new("#{line[1]}#{DELIMITER_TAIL}")
        KEYS.zip(line).to_h
      end
    end
  end
end

参考: Rails のキャッシュ機構 - Railsガイド

以下のテストを書いたのですが、2回に1回ぐらいの割合でテストが失敗しました。

class PatternTest < ActiveSupport::TestCase
  test "Patternモデル: レコード更新でキャッシュキーが更新される" do
    cache01 = cache_key(Pattern)

    patterns = Pattern.all.order(:updated_at)
    assert_equal cache01[:latest_regex], patterns.last.updated_at.to_formatted_s(:number)

    first = Pattern.first
    first.regex = '42'
    first.save

    Pattern.fetch_regex
    cache02 = cache_key(Pattern)

    # カウンタは更新されないが最終更新日は更新される
    assert_equal cache02[:count], cache01[:count]
    assert_not_equal cache02[:latest_regex], cache01[:latest_regex]
  end

  private

  def cache_key(pattern)
    return {
      latest_regex: pattern.order(:updated_at).last.regex,
      count: pattern.count
    }
  end
end

結局、原因は更新のタイムスタンプが1秒単位だったことでした(msec単位の更新は取れません)。

試しにsleep(1)を追加してタイムスタンプの更新を検出可能にしたところ、落ちなくなりました。しかしテストの実行時間が長くなるうえに、タイミング依存の問題が生じる可能性があるので、これは避けたい。

  test "Patternモデル: レコード更新でキャッシュキーが更新される" do
    cache01 = cache_key(Pattern)

    patterns = Pattern.all.order(:updated_at)
    assert_equal cache01[:latest_regex], patterns.last.updated_at.to_formatted_s(:number)

    first = Pattern.first
    first.regex = '42'
    first.save

    sleep(1) # 1秒待つ(更新タイムスタンプが秒未満を取れないため)
    Pattern.fetch_regex
    cache02 = cache_key(Pattern)

    # カウンタは更新されないが最終更新日は更新される
    assert_equal cache02[:count], cache01[:count]
    assert_not_equal cache02[:latest_regex], cache01[:latest_regex]

考えてみれば、更新のタイムスタンプを当てにするより、最新レコードの更新内容(uniqueなregex)そのものを使う方がよさそうです。

最終的に以下のようにコードとテストを両方修正したことで、sleep(1)なしでもテストが安定するようになりました。

class Pattern
  # クエリ結果をキャッシュする
  def fetch_regex
    Rails.cache.fetch("/model/pattern/check/#{Pattern.count}_#{Pattern.order(:updated_at).last.regex}", expires_in: 1.day, compress: true) do
      Pattern.pluck(*KEYS).map do |line|
        line[1] = Regexp.new("#{line[1]}#{DELIMITER_TAIL}")
        KEYS.zip(line).to_h
      end
    end
  end
end
  test 'Patternモデル: レコード更新でキャッシュキーが更新される' do
    cache01 = cache_key(Pattern)

    patterns = Pattern.all.order(:updated_at)
    assert_equal cache01[:latest_regex], patterns.last.regex

    first = Pattern.first
    first.regex = '42'
    first.save

    Pattern.new.fetch_regex
    cache02 = cache_key(Pattern)

    # レコード数は更新されないがregexは更新される
    assert_equal cache02[:count], cache01[:count]
    assert_not_equal cache02[:latest_regex], cache01[:latest_regex]
  end

  private

  def cache_key(pattern)
    return {
      latest_regex: pattern.order(:updated_at).last.regex,
      count: pattern.count
    }
  end


CONTACT

TechRachoでは、パートナーシップをご検討いただける方からの
ご連絡をお待ちしております。ぜひお気軽にご意見・ご相談ください。