Ruby: Date.parse に関連した Flaky Test を修正する


2025年 01月 30日

SI部のr_maedaです。2025年の技術者ブログはこれが初投稿のようです。
本年もタイムインターメディアの技術者ブログをよろしくお願いいたします。

自動テストが時々失敗する…

とある Ruby on Rails プロジェクトで「日付形式の文字列を受け取って処理する」クラスを作成して運用していたところ、そのクラスを対象にした自動テストが稀に失敗する事象を観測しました。

該当するプロダクションコード/テストコードは以下に示すようなものであり、「日付文字列」のパースには Date.parse を利用していました。

# プロダクションコード
class Klass
  def initialize(date_str)
    @date = Date.parse(date_str)
  rescue Date::Error
    raise ArgumentError
  end

  ...
end

# テストコード
RSpec.describe 'Klass.new' do
  subject { Klass.new(date_str) }

  context '与えられた文字列が日付として解釈できない場合' do
    let(:date_str) { Faker::AlphaNumeric.alpha }

    it { expect { subject }.to raise ArgumentError }
  end

  context '与えられた文字列が日付として解釈できる場合' do
    let(:date_str) { Faker::Date.between(...).iso8601 }

    ...
  end
end

なぜテストに失敗するのか

最初は「ランダムなアルファベット文字列が『日付』として解釈できるパターンなんてある?」と、テストが失敗する理由が分からなかったのですが、調べたり検証しているうちに、1つのパターンが見えてきました。

「もしかして、Date.parse って英語表現の日付も解釈できたりするのかな…?試しに『1月: January』を渡してみるとどうなるんだろう」

$ irb
irb(main):001:0> require 'date'
=> true
irb(main):002:0> Date.parse('January')
=> #<Date: 2025-01-01 ((2460677j,0s,0n),+0s,2299161j)>

「とはいえ、ランダム文字列が『January』になるって、相当稀だよな。もしかして、3文字の短縮表現でも大丈夫なのか?」
「先頭が小文字でも大丈夫?」
「特定の3文字から始まる場合、その後ろにどんな文字列が続いていても、パースエラーは発生しないのか??」

irb(main):003:0> Date.parse('Jan')
=> #<Date: 2025-01-01 ((2460677j,0s,0n),+0s,2299161j)>
irb(main):004:0> Date.parse('jan')
=> #<Date: 2025-01-01 ((2460677j,0s,0n),+0s,2299161j)>
irb(main):005:0> Date.parse('Janne Da Ark')
=> #<Date: 2025-01-01 ((2460677j,0s,0n),+0s,2299161j)>

「全部パースできるやん…」

実験からわかったこと

  • 十二月を表す3文字 (Jan ~ Dec)
  • 七曜を表す3文字 (Sun ~ Sat)

Date.parse は「これらの3文字から始まる文字列」が与えられた場合、「十二月」の場合は「今年のその月の初日の日付」を、「七曜」の場合は「直近のその曜日の日付」を返すようです。

また他にも、"1st" "2nd" "3rd" といった「数字から始まる序数」にも反応するようです。この場合は「当月のn日」を返しました。

irb(main):001:0> require 'date'
=> true
irb(main):002:0> Date.parse('1st')
=> #<Date: 2025-01-01 ((2460677j,0s,0n),+0s,2299161j)>
irb(main):003:0> Date.parse('first') # これには反応しない
(irb):3:in `parse': invalid date (Date::Error)
irb(main):004:0> Date.parse('1')     # 流石に「年」なのか「月」なのか「日」なのか分からず、諦める模様
(irb):4:in `parse': invalid date (Date::Error)
irb(main):005:0> Date.parse('32nd')  # 32日がある月もないので、反応する上限は 31st まで
(irb):5:in `parse': invalid date (Date::Error)

結論

受け取る「日付文字列」のフォーマットが決まっている場合は Date.iso8601Date.strptime などを使いましょう。
Date.parse は「どんなフォーマットの日付文字列が渡されてもいい感じに解釈してくれる」便利なメソッドですが、裏を返すと「想定外の挙動を示すことがある」メソッドであるとも言えます。
「想定外の挙動」は少ないに越したことはないので、思考停止で Date.parse を採用するのはやめたほうが良さそうです。

どうしても Date.parse を使う必要がある(e.g. ユーザーの画面入力値をできる限りエラーにしたくないなど)場合は、「異常な文字列が与えられた場合」のテストに「ランダムな文字列」を使うことを諦めるしかないのかなと思います。

おまけ

< 前の記事へ 次の記事へ >