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

週刊Railsウォッチ: "リーダブルテストコードについて考えよう"スライド公開、Evil Martiansが日本上陸ほか(20220801前編)

こんにちは、hachi8833です。

週刊Railsウォッチについて

  • 各記事冒頭には🔗でパーマリンクを置いてあります: 社内やTwitterでの議論などにどうぞ
  • 「つっつきボイス」はRailsウォッチ公開前ドラフトを(鍋のように)社内有志でつっついたときの会話の再構成です👄
  • お気づきの点がありましたら@hachi8833までメンションをいただければ確認・対応いたします🙏

TechRachoではRubyやRailsなどの最新情報記事を平日に公開しています。TechRacho記事をいち早くお読みになりたい方はTwitterにて@techrachoのフォローをお願いします。また、タグやカテゴリごとにRSSフィードを購読することもできます(例:週刊Railsウォッチタグ)

お知らせ: 来週および来来週の週刊Railsウォッチはお盆休みをいただきます🍉。次回は8/22(月)を予定しています。

🔗Rails: 先週の改修(Rails公式ニュースより)

今回は以下の公式更新情報とコミット差分から見繕いました。

🔗 見つからない訳文をキャッシュストアから取り出すと別オブジェクトになるのを修正

修正: #45571
Object.newをデフォルトに使うと、キャッシュストアからフェッチしたときに別のオブジェクトを取得してしまう。https://github.com/ruby-i18n/i18n/blob/5963031f2cc52847a6c431be2b2b38229b1793d2/lib/i18n/backend/cache.rb#L89
このため、それなりに適当な数値をデフォルトに使った。
同PRより

# actionview/lib/action_view/helpers/translation_helper.rb#L121
      private
-       MISSING_TRANSLATION = Object.new
+       MISSING_TRANSLATION = -(2**60)
        private_constant :MISSING_TRANSLATION

つっつきボイス:「ここでObject.newする必要がないのは当然として、この-(2**60)というマジックナンバーはどうやって決めたんだろう?🤔」「何か意味がありそう」「コメントを見ると、最初は適当な1152921504606846976という数値だったけど検証しやすい-(2**60)に置き換えたらしい」

参考: マジックナンバー (プログラム) - Wikipedia

「ビット列を発見しやすくするためだとしたら、全部のビットに1が立っていたりしそう」「マジックナンバーというと、よく0xDEADBEEF("dead beef")のようなデバッグ時に検索しやすい値を使ったりしますよね↓」「なるほど、死んだ牛🐮」「16進数のA〜Fを使って言葉遊び的にマジックナンバーを決めるんですね」「他にも"feed face"や"feel dead"みたいなのもあるのね」

参考: Hexspeak - Wikipedia

-(2**60)をirbで16進数表示してみたらこんな値になった↓」

puts sprintf("%#x", -(2**60))
0x..f000000000000000

「なお、"dead beef"のような語順に意味のある2ワードをマジックナンバーにしておくと、エンディアンの違いも判別できます」「逆順の"beef dead"ならリトルエンディアン、通常の"dead beef"ならビッグエンディアンということですね」

参考: エンディアン - Wikipedia

🔗 SecurePasswordの改善2つ

  • SecurePasswordに、:if:unless:onをキーに持つHashを渡せるようになった。これにより、特定の条件に応じてバリデーションを柔軟にトリガーしたりスキップしたりできるようになる。
secure_password validations: {if: :requires_password?}`

Kevin Jacoby
Changelogより


つっつきボイス:「関連していそうなプルリク2つをまとめました」「1つ目は、has_secure_password validations: { if: :requires_password }のようにバリデーションのオプションをハッシュ形式で渡せるようにしたんですね」

# activemodel/lib/active_model/secure_password.rb#L83
      def has_secure_password(attribute = :password, validations: true)
        # Load bcrypt gem only when has_secure_password is used.
        # This is to avoid ActiveModel (and by extension the entire framework)
        # being dependent on a binary library.
        begin
          require "bcrypt"
        rescue LoadError
          $stderr.puts "You don't have bcrypt installed in your application. Please add it to your Gemfile and run bundle install."
          raise
        end
        include InstanceMethodsOnActivation.new(attribute)
        if validations
          include ActiveModel::Validations

+         validation_options = validations.is_a?(Hash) ? validations : {}

          # This ensures the model has a password by checking whether the password_digest
          # is present, so that this works with both new and existing records. However,
          # when there is an error, the message is added to the password attribute instead
          # so that the error message will make sense to the end-user.
-         validate do |record|
+         validate(validation_options) do |record|
            record.errors.add(attribute, :blank) unless record.public_send("#{attribute}_digest").present?
          end

          validate do |record|
            if challenge = record.public_send(:"#{attribute}_challenge")
              digest_was = record.public_send(:"#{attribute}_digest_was") if record.respond_to?(:"#{attribute}_digest_was")
              unless digest_was.present? && BCrypt::Password.new(digest_was).is_password?(challenge)
                record.errors.add(:"#{attribute}_challenge")
              end
            end
          end
          validates_length_of attribute, maximum: ActiveModel::SecurePassword::MAX_PASSWORD_LENGTH_ALLOWED
          validates_confirmation_of attribute, allow_blank: true
        end
      end

このプルリクは、has_secure_passwordを拡張してpassword_challengeアクセサと適切なバリデーションを定義する。password_challengeが設定されている場合、現在永続化済みpassword_digest(すなわちpassword_digest_was)とマッチするかどうかをバリデーションする。
これにより、パスワードのチャレンジをパスワード確認と同じように手軽に実装できるようになり、コントローラと同じエラーハンドリングロジックをビューで再利用できるようになる。たとえばコントローラで以下のように書く代わりに、

password_params = params.require(:password).permit(
  :password_challenge,
  :password,
  :password_confirmation,
)

password_challenge = password_params.delete(:password_challenge)
@password_challenge_failed = !current_user.authenticate(password_challenge)

if !@password_challenge_failed && current_user.update(password_params)
  # ...
end

以下のように書ける。

password_params = params.require(:password).permit(
  :password_challenge,
  :password,
  :password_confirmation,
).with_defaults(password_challenge: "")

if current_user.update(password_params)
  # ...
end

さらに、ビューで@password_challenge_failedをわざわざチェックしなくても他のフォームのフィールドエラーと同じようにパスワードチャレンジをレンダリングできるし、config.action_view.field_error_procも利用できる。
同PRより

「2つ目は、パスワード変更画面によくある"現在のパスワード"と"新しいパスワード"と"新しいパスワードの確認"のバリデーションを公式にサポートしたようですね」「こういうのがRailsに入るのはよさそう👍」「何となくDeviseを使わずにやれる方向に進んでいる気もする」

参考: has_secure_password -- ActiveModel::SecurePassword::ClassMethods

🔗 PostgreSQL拡張機能に依存するオブジェクトがある場合は削除しないようになった

PostgreSQL拡張機能に依存するオブジェクトがある場合は拡張機能を削除しないようにする。
従来は拡張機能を削除すると暗黙で依存オブジェクトも削除されていたが、エラーを出力するようになった。
なお、以下を指定することで拡張機能を強制削除できる。

disable_extension :citext, force: :cascade

修正: #29091
fatkodima
同Changelogより


つっつきボイス:「issue #29091を見ると、マイグレーションのchangeenable_extensionを書いてdownすると拡張機能が消えるという問題があったのね↓」「踏むと悲しいヤツですね」「こういう書き方はあまり想定したくないだろうけど、事故ったときの影響が大きいので対策を入れたんでしょうね」「修正では古いマイグレーションの互換性も確保しているそうです」

class CreateItems < ActiveRecord::Migration
  def change
    enable_extension 'citext'
    create_table :items do |t|
      …
      t.citext      :code
      …
    end
    …
  end
end

class CreateMembers < ActiveRecord::Migration
  def change
    enable_extension 'citext'
    create_table :members do |t|
      …
      t.citext      :email
      …
    end
    …
  end
end

🔗 ActiveStorage::Blobのアナライズ後にモデルのレコードをtouchするよう修正

ActiveStorage::Blobでアナライズすると、対応するモデルの全レコードをtouchする。
これにより、レコードをリクエストしてキャッシュエントリをビルドすると、最初のanalyze_laterが完了する前にキャッシュが無効にならない可能性がある(他の何かがそのレコードを更新すると無効になる)という競合状態が修正される。また、blobが再度アナライズされたときにもキャッシュが無効化されるようになるので、アナライザのバグが修正された場合や新しいアナライザが追加された場合に有用。
Nate Matykiewicz
同Changelogより


つっつきボイス:「Active Storageの改修です」「画像や動画に対するアナライズみたい」「ActiveStorage::Blobafter_updateフックを追加してレコードの更新後にtouchを実行することで、analyzeするときのキャッシュの競合状態を解消したようですね」

# activestorage/app/models/active_storage/blob.rb#L55
+ after_update :touch_attachment_records
...
  private
...
+   def touch_attachment_records
+     attachments.includes(:record).each do |attachment|
+       attachment.touch
+     end
+   end

参考: Rails API ActiveStorage::Blob::Analyzable

🔗 ドキュメント: Active Recordクエリガイドに始端/終端なしrangeのサンプルを追加


つっつきボイス:「ガイドの更新です」「Ruby 2.6や2.7で入った始端なしrangeや終端なしrangeの記述が追加されたのね」

guides/source/active_record_querying.md#660
始端なしrangeや終端なしrangeがサポートされたことで、以下のような大なり小なり条件を書けるようになる。

Book.where(created_at: (Time.now.midnight - 1.day)..)

上は以下のようなSQLを生成する。

SELECT * FROM books WHERE books.created_at >= '2008-12-21 00:00:00'

「ところで、1..みたいな終端なしrangeは普通に使うけど、..5みたいな始端なしrangeをたまに見かけると一瞬考え込んじゃうかも」

参考: class Range (Ruby 3.1 リファレンスマニュアル)

🔗Rails

🔗 『リーダブルテストコードについて考えよう』のスライドが公開


つっつきボイス:「ツイートに書いたとおり、いいスライドでした」「jnchitoさんがこの間から準備していたイベントのスライドですね」「大事な話が盛りだくさん👍」

「そうそう、自分もまさにこういうふうにテストを書きますし↓、場合によってはコメントにカレンダーも書きますね」

「カレンダーまで書いちゃいますか」「たとえば土日祝日を除いて日数をカウントする機能のテストだったら、コメントのカレンダーに"この日を祝日とする場合"と書いたりします」「あ〜なるほど」「極力そういうふうに具体的なテストを書いておかないと、実装のときに自分が混乱しそうになる(このサンプルコードではそこまでしなくていいと思いますが)」

「テストコードはDRYにしないでベタに読みやすく書くべきという主張、本当にそのとおり👍」「同じく」「自分がfactory_botよりもfixtureを使いたい理由のひとつもそれで、factory_botでも固定値を使っていればいいんですが、生成したランダムなデータで落ちると、原因を突き止めるのにすごく手間取ることがあるんですよ」「わかります」「なお、factory_botやfixtureを使う場合、factory_botやfixtureのデータを修正するとテストコードも修正が必要になってつらくなることもありますが、これは仕方がないでしょうね」

thoughtbot/factory_bot - GitHub

Rails 7 API: ActiveRecord::FixtureSet(翻訳)

「今回のイベントはこのぐらい気合を入れて準備していたそうです↓」「リハーサル回数すごい」「特に何かを実際に動かす発表はリハーサルしないと怖い」「スライドだけの発表もリハーサルしておかないと時間どおりに終われなくなったりしますね」

「他の登壇者のスライドも公開されていました↓」

「何が正しいのかが書かれていないテストコードがfailすると本当につらい」「まったくです」「一般にテストコードの改善は自分だけでするよりも、他の人にレビューしてもらうことで気づけることが多いですね」


つっつきの後にjnchitoさん自らまとめ記事を出していました↓。

🔗 Evil Martiansが大阪江戸堀にオフィスをオープン


つっつきボイス:「え、あのEvil Martiansが日本オフィス?」「しかも大阪だそうです」「合同会社イービルマーシャンズという表記にしたんですね」「思わずTwitterとLinkedInでフォローしちゃいました」「オフィスにセガメガドライブを置きたいってQiitaに書かれてますね」「そういえばRubyKaigiでその話を見かけたかも」

クジラに乗ったRuby: Evil Martians流Docker+Ruby/Rails開発環境構築(更新翻訳)

🔗 RailsのTimeでタイムゾーンを無効にする


つっつきボイス:「Rails 4をRails 5にアップグレードしたら時刻が10/11時間ずれたので、以下のような感じでタイムゾーンを無視するように変えたのか」「そういえばそういう変更があったかも」「アプリケーションの運用中にタイムゾーンが変わったらつらすぎる...」「RailsのTimeはタイムゾーンありがデフォルトですね」

# 同記事より
source: https://api.rubyonrails.org/v5.2.3/classes/ActiveRecord/Timestamp.html

# タイムゾーンをスキップする
class Topic < ActiveRecord::Base
  self.skip_time_zone_conversion_for_attributes = [:written_on]
end

# PostgreSQLを意識したタイムゾーン
ActiveRecord::Base.time_zone_aware_types += [:tsrange, :tstzrange]

🔗 canvas-lms: Railsで書かれた教育機関向け学習管理システム

instructure/canvas-lms - GitHub


つっつきボイス:「先週つっつきに参加したsakaharaさんが、このcanvas-lmsを使ったことがあると言っていたので取り上げてみました」「LMSはいわゆる学習管理システムですね: 授業で生徒が課題を受け取ったり提出したり集計したり資料を配布したりするのに使うヤツ」「ですです」

参考: Learning management system - Wikipedia

🔗 その他Rails


つっつきボイス:「APIのドキュメント生成機能を比較する記事のようで、Swagger UIとPostmanとReDocを比較してます」「APIドキュメントを自動生成することを考えると、OpenAPIのSwagger-UIがよく使われるしgemもありますけど、yamlファイルに直接ドキュメントを書く方が楽だったりもしますね」

サンプル1: Swagger Petstore -- Swagger UI
サンプル2: Swagger Petstore | Swagger Petstore | Postman API Network
サンプル3: ReDoc Interactive Demo

参考: OpenAPIとSwagger 入門

「記事ではSwagger-UIのスタイルが好きじゃないと書いていますね」「その気持ちもわかるし自社内なら何を使ってもいいと思いますけど、OpenAPIは業界標準なので顧客向けに導入しやすいんですよ」「そうそう、OpenAPIにしてダメと言われることはないでしょうね」


「ツイートに引用されているヨビノリさんの動画が面白くて最近ハマってます」「ヨビノリは"予備校のノリで学ぶ"の略なんですね」

「勉強していろんなことを学べば学ぶほどわからないことが増えるというのは、ホントその通り」「今まで見えていなかったものが見えれば見えるほどそうなりますよね」「動画すごくいいこと言ってる気がする」「化物語のセリフも引用されてた↓」

参考: 何でもは知らないわよ。知ってることだけの元ネタ - 元ネタ・由来を解説するサイト 「タネタン」

「大学で初めて論文を読んだときのわからなさは半端なかった」「興味があって読んでるはずなのに、知らない用語だらけだし、参照先を読んでもわからないことだらけだし」「こういう"わからない爆発"と、"それでも読んでいるうちに自分の中にだんだん何かが養われる"感覚は学習しているうちに体感するようになりますね」


前編は以上です。

バックナンバー(2022年度第3四半期)

週刊Railsウォッチ: 中高生国際Rubyプログラミングコンテスト2022、W3Cの分散型識別子仕様が勧告にほか(20220726後編)

今週の主なニュースソース

ソースの表記されていない項目は独自ルート(TwitterやはてブやRSSやruby-jp SlackやRedditなど)です。

Rails公式ニュース

RubyFlow

160928_1638_XvIP4h


CONTACT

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