- Ruby / Rails関連
週刊Railsウォッチ: "リーダブルテストコードについて考えよう"スライド公開、Evil Martiansが日本上陸ほか(20220801前編)
こんにちは、hachi8833です。
お知らせ: 来週および来来週の週刊Railsウォッチはお盆休みをいただきます🍉。次回は8/22(月)を予定しています。
🔗Rails: 先週の改修(Rails公式ニュースより)
今回は以下の公式更新情報とコミット差分から見繕いました。
- 公式更新情報: Ruby on Rails — Bugfixes, performance improvements and more!
- コミット差分: Comparing @{2022-07-22}...main@{2022-07-28} · 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"みたいなのもあるのね」
「-(2**60)
をirbで16進数表示してみたらこんな値になった↓」
puts sprintf("%#x", -(2**60))
0x..f000000000000000
「なお、"dead beef"のような語順に意味のある2ワードをマジックナンバーにしておくと、エンディアンの違いも判別できます」「逆順の"beef dead"ならリトルエンディアン、通常の"dead beef"ならビッグエンディアンということですね」
🔗 SecurePasswordの改善2つ
- PR: Allow passing Hash on secure password validations by liljack · Pull Request #45487 · rails/rails
- 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を見ると、マイグレーションのchange
にenable_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::Blob
にafter_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
🔗 『リーダブルテストコードについて考えよう』のスライドが公開
これは良い話 / リーダブルテストコードhttps://t.co/fu2PaiwO9n
— すろっくさん (@srockstyle) July 28, 2022
つっつきボイス:「ツイートに書いたとおり、いいスライドでした」「jnchitoさんがこの間から準備していたイベントのスライドですね」「大事な話が盛りだくさん👍」
「そうそう、自分もまさにこういうふうにテストを書きますし↓、場合によってはコメントにカレンダーも書きますね」
「カレンダーまで書いちゃいますか」「たとえば土日祝日を除いて日数をカウントする機能のテストだったら、コメントのカレンダーに"この日を祝日とする場合"と書いたりします」「あ〜なるほど」「極力そういうふうに具体的なテストを書いておかないと、実装のときに自分が混乱しそうになる(このサンプルコードではそこまでしなくていいと思いますが)」
「テストコードはDRYにしないでベタに読みやすく書くべきという主張、本当にそのとおり👍」「同じく」「自分がfactory_botよりもfixtureを使いたい理由のひとつもそれで、factory_botでも固定値を使っていればいいんですが、生成したランダムなデータで落ちると、原因を突き止めるのにすごく手間取ることがあるんですよ」「わかります」「なお、factory_botやfixtureを使う場合、factory_botやfixtureのデータを修正するとテストコードも修正が必要になってつらくなることもありますが、これは仕方がないでしょうね」
「今回のイベントはこのぐらい気合を入れて準備していたそうです↓」「リハーサル回数すごい」「特に何かを実際に動かす発表はリハーサルしないと怖い」「スライドだけの発表もリハーサルしておかないと時間どおりに終われなくなったりしますね」
逆に練習するから上手になるのかも。ちなみに発表前に録画していたリハーサル動画の本数を数えたら全部で13本ありましたw #vstat https://t.co/sNbOP1ipwv pic.twitter.com/nr7YiooFQO
— Junichi Ito (伊藤淳一) (@jnchito) July 28, 2022
「他の登壇者のスライドも公開されていました↓」
「何が正しいのかが書かれていないテストコードがfailすると本当につらい」「まったくです」「一般にテストコードの改善は自分だけでするよりも、他の人にレビューしてもらうことで気づけることが多いですね」
つっつきの後にjnchitoさん自らまとめ記事を出していました↓。
DRYで技巧的なテストコード、書いた直後は理解できるんよ(本人だけは)。自己満足度もめっちゃ高くなるしw でも、3年後に読み直すともうダメ。「誰やねん、こんなわかりにくいテストコード書いたのは!!あ、俺だった……」ってなります(経験者談)。
— Junichi Ito (伊藤淳一) (@jnchito) July 31, 2022
約20年前に書かれたweb記事ですが、先日僕が「リーダブルテストコード」の登壇で言いたかったことがほぼ網羅されていてビックリしました。 #vstat
JUnit 実践講座 https://t.co/eh6PvyMyhZ
— Junichi Ito (伊藤淳一) (@jnchito) July 30, 2022
でも結局のところ、AAAの話も「テストコードにおいて、過度なDRYは読みやすさの敵」というルールで説明がつくんですけどね。 #vstat pic.twitter.com/cQ5hNqkp6x
— Junichi Ito (伊藤淳一) (@jnchito) July 31, 2022
🔗 Evil Martiansが大阪江戸堀にオフィスをオープン
Evil MartiansのバックエンドエンジニアのAndrey Novikov (Envek)が、日本のエンジニアリングチームを強化するために大阪に!
AndreyはYabedaやafter_commit_everywhereといったOSSプロダクトをいくつか開発しており、after_commit_everywhereは最近Sidekiqでも採用されています!
ようこそ、Andrey! pic.twitter.com/fryZhEHXUy
— 合同会社イービルマーシャンズ (@evilmartians_jp) July 25, 2022
つっつきボイス:「え、あのEvil Martiansが日本オフィス?」「しかも大阪だそうです」「合同会社イービルマーシャンズという表記にしたんですね」「思わずTwitterとLinkedInでフォローしちゃいました」「オフィスにセガメガドライブを置きたいってQiitaに書かれてますね」「そういえばRubyKaigiでその話を見かけたかも」
🔗 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で書かれた教育機関向け学習管理システム
つっつきボイス:「先週つっつきに参加した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
「記事ではSwagger-UIのスタイルが好きじゃないと書いていますね」「その気持ちもわかるし自社内なら何を使ってもいいと思いますけど、OpenAPIは業界標準なので顧客向けに導入しやすいんですよ」「そうそう、OpenAPIにしてダメと言われることはないでしょうね」
なぜそうなるのか図で説明した@YobinoriさんのYouTube動画がオススメです!! #Railsチュートリアル
【5分ください】無駄に勉強モチベを下げないために https://t.co/6xOozT06Ki
> 「勉強すればするほど分からないことが増える」と悩む時期がよく来ます。それは普通なことだよということを伝えます。 https://t.co/KmJ5gH1usi
— 安川要平/Yohei Yasukawa (@yasulab) July 27, 2022
「ツイートに引用されているヨビノリさんの動画が面白くて最近ハマってます」「ヨビノリは"予備校のノリで学ぶ"の略なんですね」
「勉強していろんなことを学べば学ぶほどわからないことが増えるというのは、ホントその通り」「今まで見えていなかったものが見えれば見えるほどそうなりますよね」「動画すごくいいこと言ってる気がする」「化物語のセリフも引用されてた↓」
参考: 何でもは知らないわよ。知ってることだけの元ネタ - 元ネタ・由来を解説するサイト 「タネタン」
「大学で初めて論文を読んだときのわからなさは半端なかった」「興味があって読んでるはずなのに、知らない用語だらけだし、参照先を読んでもわからないことだらけだし」「こういう"わからない爆発"と、"それでも読んでいるうちに自分の中にだんだん何かが養われる"感覚は学習しているうちに体感するようになりますね」
前編は以上です。
バックナンバー(2022年度第3四半期)
週刊Railsウォッチ: 中高生国際Rubyプログラミングコンテスト2022、W3Cの分散型識別子仕様が勧告にほか(20220726後編)
- 20220725前編 RailsConf 2022の動画が公開、マイクロサービスのテスト戦略ほか(
- 20220719 RubyのGCが高速化、RuboCopのストレスを減らす4つの方法、Defensive CSSほか
- 20220711前編 AR::RelationにCTEを利用できるwithメソッドが追加、Propshaftアップグレードガイドほか
- 20220705後編 6月のRubyコア動向、Stack Overflowアンケート結果ほか
- 20220704前編 マイグレーションをStrategyパターンで拡張可能にほか
今週の主なニュースソース
ソースの表記されていない項目は独自ルート(TwitterやはてブやRSSやruby-jp SlackやRedditなど)です。
週刊Railsウォッチについて
TechRachoではRubyやRailsなどの最新情報記事を平日に公開しています。TechRacho記事をいち早くお読みになりたい方はTwitterにて@techrachoのフォローをお願いします。また、タグやカテゴリごとにRSSフィードを購読することもできます(例:週刊Railsウォッチタグ)