give IT a try

プログラミング、リモートワーク、田舎暮らし、音楽、etc.

僕がRSpecでsubjectを使わない理由

はじめに

僕は折に触れて「RSpecではなるべくsubjectを使わない方がいい」という発言をしています。

ただ、その理由をあまり明確に明文化していなかったので、ここらでちょっとブログにまとめてみたいと思います。

RSpecをよく知らない方へ

そもそもRSpecって何?subjectって何?という方は以下の記事を読んでから戻ってくることをお勧めします。 qiita.com

subjectの使いどころ = メソッドの戻り値をテストするケース

subjectの使いどころについては、以前僕が書いたQiita記事に投稿された、こちらのコメントが的を射ていると思います。

副作用が目的の(procedural)メソッドと、返り値が目的の(functional)メソッド。 返り値が目的のメソッドは、subjectがうまくハマる。

by @akihitofujiwara 2017-09-15 14:36

上のコメントにあるとおりなのですが、メソッドの戻り値をテスト対象にするときはまだ収まりが良いです。たとえば、イメージとしてはこんな感じです。

def greet
  'Hello!'
end

describe '#greet' do
  subject { greet }
  it { is_expected.to eq 'Hello' }
end

上のテストコードはgreetメソッドをテストしています。このメソッドはHello!という文字列を返すメソッドです。このように戻り値を返すメソッドをsubjectにすると、あまり大きな問題はありません。

subjectが向いていないケース = メソッドの副作用をテストするケース

一方で、次のようなメソッドはsubjectには向いていません。

class Counter
  attr_reader :count

  def initialize
    @count = 0
  end

  def increment
    @count += 1
  end
end

describe 'Counter#increment' do
  let(:counter) { Counter.new }
  subject { counter.increment }
  it do
    subject
    expect(counter.count).to eq 1
  end
end

上のテストコードはCounterクラスのincrementメソッドをテストしています。そのため、subjectにはcounter.incrementをセットしました。しかし、incrementメソッドはあくまでインスタンス内で保持しているカウント値を増やすことが目的です(注:Rubyの言語仕様上、加算後の値がメソッドの戻り値になりますが、説明の都合上、戻り値はvoid、つまり戻り値無しとします)。つまりこのメソッドは副作用を起こすことを目的としています。subjectは副作用を起こすメソッドのテストには向いていないため、次のような不自然なテストコードが生まれます。

it do
  # 副作用を発生させる
  subject
  # 副作用の結果を検証する
  expect(counter.count).to eq 1
end

もちろん、ひとひねりすれば次のようなコードを書くこともできます。

describe 'Counter#increment' do
  let(:counter) { Counter.new }
  subject { ->{ counter.increment } }
  it { is_expected.to change(counter, :count).by(1) }
end

これを「美しい」と思う人もいるかもしれませんが、僕からすると「無理矢理がんばってるなあ」感が強いです。あくまで主観の問題かもしれませんが、こういったテストコードは「自己満足」の要素が強い気がするんですよね。で、その自己満足と引き換えに、以下のようなデメリットが発生します。

  • subjectをがんばって使うための、試行錯誤の工数が発生する
  • そのがんばりの結果、摩訶不思議なテストコードが生まれる(RSpec初心者に優しくないし、熟練者が見ても一瞬考えてしまう)

そのため、もしチーム内で「必ずsubjectを使うこと」というコーディング規約があったりすると、上のような問題が頻発しそうな気がします(個人の推測です)。

僕の主張:そもそもsubjectなしで書けばいいじゃない

上で示したテストコードはどちらもsubjectなしで書いても全然問題ないと思います。実際にsubjectなしのテストコードに書き直してみましょう。

最初はメソッドの戻り値をテストする場合です。

describe '#greet' do
  it 'returns "Hello!"' do
    expect(greet).to eq 'Hello!'
  end
end

次に、メソッドの副作用をテストする場合です。

describe 'Counter#increment' do
  let(:counter) { Counter.new }
  it 'increments count' do
    counter.increment
    expect(counter.count).to eq 1
  end
end

別解としてこういう書き方もあります。

describe 'Counter#increment' do
  let(:counter) { Counter.new }
  it 'increments count' do
    expect { counter.increment }.to change(counter, :count).by(1)
  end
end

僕はこういうテストコードでも何も問題を感じません。むしろシンプルかつ素直なので、subjectを使うときよりもコードが読みやすいと感じます。

subjectを好む人の主張に、「テスト対象のメソッドがわかりやすくなる」というものがありますが、describe '#greet'describe 'Counter#increment'でテスト対象を明示しておけば、それで十分明確になるはずです。よって、「テスト対象をわかりやすくする」という目的において、subjectは必ずしも必要ないと考えています。

もうひとつの問題:subjectを使うと視線を上下させなければならない

subjectを使うもう一つの問題点は、subjectを使うタイミングで視線を上下させなければならないところです。上で示したような単純なサンプルコードとは異なり、実務で書くテストコードはどうしても大きくなりがちです。そうすると、いちいち「このsubjectっていったい何だっけ?」というのを確認しに行く作業が発生します。

イメージとしてはこんな感じです。

describe 'Foo#bar' do
  let(:foo) { Foo.new }
  subject { foo.bar }
  context 'when A' do
    before do
      # 何行にも渡ってセットアップが続く
      # .
      # .
      # .
    end
    it { is_expected.to eq 'abc' }
  end
  context 'when B' do
    before do
      # 何行にも渡ってセットアップが続く
      # .
      # .
      # .
    end
    it { is_expected.to eq 'def' }
  end
  context 'when C' do
    before do
      # 何行にも渡ってセットアップが続く
      # .
      # .
      # .
    end
    it { is_expected.to eq 'xyz' }
  end
end

こんなテストコードになっていると、context 'when C'を読む頃には「あれ?subjectって何だっけ??」ということになってしまいます。

テストコードは人間が読むドキュメントのように上から下に読めるようにすべきです。subjectをなくせば、視線を上下させる必要がなくなります。

context 'when C' do
  before do
    # 何行にも渡ってセットアップが続く
    # .
    # .
    # .
  end
  it 'returns "xyz"' do
    # 視線を上に戻さなくてもテストコードが理解できる
    expect(foo.bar).to eq 'xyz'
  end
end

プルリクエストをレビューするときもsubjectの実体がdiffに出てこない(ことが起こりえる)

プルリクエストをコードレビューするときはこんなふうに新規に追加したテストケースだけがdiffに上がってきて、subjectの実体はdiffに出てこないこともあるかもしれません。

   end
+  context 'when C' do
+    before do
+      # 何行にも渡ってセットアップが続く
+      # .
+      # .
+      # .
+    end
+    it { is_expected.to eq 'xyz' }
+  end
 end

subjectを使わなければそのテストで何の結果を検証しているのか、diffを見るだけで理解できます。

   end
+  context 'when C' do
+    before do
+      # 何行にも渡ってセットアップが続く
+      # .
+      # .
+      # .
+    end
+    it 'returns "xyz"' do
+      expect(foo.bar).to eq 'xyz'
+    end
+  end
 end

応用:subjectの代わりにrspec-parameterizedを使う

かつては僕もたまにsubjectを使っていました。それは次のようなテストコードを書く場合です。

describe 'Ticket.calc_fee' do
  subject { Ticket.calc_fee(age) }
  context '子どもの場合' do
    let(:age) { 10 }
    it { is_expected.to eq 500 } # 半額
  end
  context '大人の場合' do
    let(:age) { 20 }
    it { is_expected.to eq 1000 } # 通常料金
  end
  context '老人の場合' do
    let(:age) { 65 }
    it { is_expected.to eq 0 } # 無料
  end
end

これはメソッドに渡す引数をcontextごとにいろいろ変えながら、メソッドの戻り値を検証するテストコードです(「同値分割・境界値分析」みたいなテスト技法の話はこのエントリの本題ではないため、ここでは無視します)。

ですが、こういったテストコードはrspec-parameterized gemを使った方がシンプルに書けます(余談:gemなしで使えるようにRSpecの標準機能に取り込んでほしい〜)。

describe 'Ticket.calc_fee' do
  where(:age, :fee) do
    [
      [10, 500],
      [20, 1000],
      [65, 0]
    ]
  end

  with_them do
    it 'returns fee by age' do
      expect(Ticket.calc_fee(age)).to eq fee
    end
  end
end

rspec-parameterizedすら使わず雑に書く

上のような例であればそもそもsubjectもrspec-parameterizedもいらないかもしれません。

「デグレを防止できる」「APIドキュメント代わりになる」という要件を満たすのであれば次のようなテストコードでも必要十分だったりします。

describe 'Ticket.calc_fee' do
  it 'returns fee by age' do
    # 子どもの場合
    expect(Ticket.calc_fee(10)).to eq 500
    # 大人の場合
    expect(Ticket.calc_fee(20)).to eq 1000
    # 老人の場合
    expect(Ticket.calc_fee(65)).to eq 0
  end
end

最近はこういうスタイルで書くことが多いので、僕が書くRSpecのコードではsubjectが登場する機会がほとんどなくなりました。

参考:RSpecのメンテナから見たsubject

日本語には翻訳されていませんが、RSpecの元メンテナ・Myron Marston氏が執筆した"Effective Testing with RSpec 3"という本があります。

本書を読むとsubjectについて次のような注意書きが書いてあります。

We recommend you use this one-liner syntax sparingly. It’s possible to over-emphasize brevity and rely too much on one-liners.

(僕の日本語訳)
ワンライナー構文(訳注 it { is_expected.to eq 'Hello!' }のような構文)は控えめに使用することをお勧めします。この構文を使うと簡潔さを過剰に求めてしまい、大量のワンライナーが量産されてしまう恐れがあります。

本書に書かれているsubjectに関する注意点は、以下の翻訳記事でもほぼ同じ内容が語られています。

qiita.com

私はsubjectやワンライナー構文(例: it { is_expected...})を使うことはほとんどありません。特定の分野のプロジェクト(例:mustermann)ではワンライナー構文はうまく機能しますが、大半のプロジェクトではあまり有益なテクニックではないと考えています。

【翻訳】RSpecのリードメンテナだけど何か質問ある? - Qiita

まとめ

というわけで、このエントリでは僕がRSpecでsubjectを使わない理由をつらつらと書いてみました。

このあたりは個人の好みで大きく分かれるというか、もはや宗教に近いものを感じることもよくあります。「絶対subject使うマン」から見ると、僕の主張はどれも眉をひそめる話ばかりだったんじゃないかと思います。なので、「subjectを使った方がいい!」と思っている人はそのまま使い続けても構いません。

「subjectを使いたくない」と思っていても、既存のプロジェクトにあとから参加した場合は悩ましいですね。既存のテストコードが「subject使いまくり」だと、テストコードの一貫性の面でsubjectを無くすのは難しいかもしれません。その場合はチーム内でテストコードの方針を話し合ってみてください。

あわせて読みたい

テストコードをテクニカルに書くために時間をかけすぎるのは不毛だと僕は考えています。 qiita.com

テクニカルなテストコードを避け、上から下に読めるテストコードを書きましょう、という話をここに書いています。 qiita.com

本文でも引用した"Effective Testing with RSpec 3"という書籍の書評です。 blog.jnito.com

[PR] RailsですらすらとRSpecのコードが書けるようになりたい!という方はこちらの電子書籍をどうぞ💁‍♂️(僕が翻訳しています) leanpub.com

【2021年版】M1 MacでBlu-rayビデオを再生する方法

はじめに

昨年末ぐらいから僕がどっぷりハマってるバンド、赤い公園のBlu-ray版LIVEビデオを買いました。

ただ、我が家にはBlu-rayプレイヤーがありません。というわけで、手持ちのM1 MacでBlu-rayビデオを再生しようと思いました。

DVDならApple純正のSuperDriveやDVDプレイヤーソフトが使えるので互換性問題を気にしなくていいのですが、Blu-rayに関してはApple純正のBlu-rayドライブやプレイヤーソフトがありません。

MacでBlu-rayを再生する方法をネットを検索するといろいろ情報は見つかるのですが、情報がちょっと古かったり、M1 Macじゃなかったり、具体的なBlu-rayドライブの型番が書かれていなかったりして、実際にMacで再生できるまで「ほんとに再生できるのかな?」と不安でした。

結果としては無事に再生できたのですが、同じように不安に思ってるM1 Macユーザーさんのために、このエントリで僕の再生環境を詳しくまとめておきます。

Blu-rayドライブ = ロジテック LBD-LPWAWU3CNDB

Blu-rayドライブはロジテックのLBD-LPWAWU3CNDBを買いました。Amazonで7,722円でした。

「メーカーがいちおうMac対応をうたっている(ただしM1 Mac対応とは書いてない)」「USB Type-CでMacに接続できる」「価格がお手頃」という理由でこれを選びました。

Amazonのレビューを見ると「作動音がうるさい」というようなコメントが散見されるのでちょっと心配していましたが、ビデオの再生中は特に作動音は気になりませんでした。耳をドライブに近づけるとかすかに「シャーッ」という回転音が聞こえる程度です。ただし、ディスクを入れて最初の読み取り時にはカチャカチャ、キュインキュインと多少音がします。

Blu-rayプレイヤーソフト = Mac Blu-ray Player

Blu-rayビデオはBlu-rayドライブだけでは再生できません。別途プレイヤーソフトの購入が必要にになります。これは"Mac Blu-ray Player"というソフトを購入しました。価格は3,795円です。

通常版とPRO版の2種類があって、比較表を見ても具体的にどういうケースで違いが出るのかよくわからないのですが、とりあえず安い方の通常版で普通にBlu-rayビデオを再生することができました。赤い公園のBlu-rayにはメンバーによる副音声トークも収録されているのですが、これも問題なく再生できました。

なお、"Mac Blu-ray Player"は無料版をダウンロードできるので、実際に購入する前に自分のMacで再生できるか確認できます。無料版は画面上に「未登録です」というような透かしが表示されるだけで、機能的には一通り使えるみたいです(もし継続して使うならちゃんと購入しましょう!)。

参考:Mac本体 = MacBook Pro (13-inch, M1, 2020) / macOS Big Sur 11.6

いちおう僕が使っているMacも紹介しておきます。僕のMacはMacBook Pro (13-inch, M1, 2020)で、OSはmacOS Big Sur 11.6です。

このMacにEIZOのEV2785-BKという27インチ4Kモニタを接続しています。

Mac本体でもサブディスプレイでも動画は再生できます。

ついでにいうと、音声はDynaudio Music 1というBluetoothスピーカーから再生しています。こちらも問題なく音声を再生することができます。

このスピーカーで聴くとBlu-rayだからなのか、YouTube動画なんかの音声に比べるとすごく音の密度や情報量が多い気がします。ライブビデオを視聴するのには打ってつけですね!

まとめ

というわけで、まとめるとM1 MacでBlu-rayビデオを再生するのに必要なものは以下の2つです。

  • Blu-rayドライブ(ロジテック LBD-LPWAWU3CNDB) = 7,722円
  • Blu-rayプレイヤーソフト(Mac Blu-ray Player) = 3,795円

合計すると11,517円(税込)でM1 MacのBlu-ray再生環境を整えることができました。

M1 MacでBlu-rayビデオを見たいと思ってる人は参考にしてみてください!

人にはヒトの副反応。

はじめに

新型コロナワクチン接種が2回終わりました。接種前は「副反応怖い〜。ガクガクブルブル😱」とビビってましたが、終わってみたら「想像してたよりはマシ」という感想でした。

自分への備忘録と、これから接種する人への参考情報として僕が体験した副反応をまとめておきます。もちろん、これはあくまで「僕の場合」です。副反応は人によって大きく個人差があることをお忘れなく。

f:id:JunichiIto:20210922081140j:plain

データ

  • 44歳男性、持病や基礎疾患はなし
  • ワクチンの製薬会社 = ファイザー
  • 接種1回目 2021年8月26日(木)午前11時頃
  • 接種2回目 2021年9月16日(木)午前11時頃

1回目の副反応

  • 腕が痛いと言えば痛いけど、思っていたほどではない。先に打ってた妻の方がずっと痛そうだった。
  • 熱は当日に36.8℃ぐらいまで上がった。少し身体が熱いけど、日常生活には支障なし。
  • 当日から翌日にかけて軽い頭痛が続いた。「寝過ぎて頭痛い」っていうときの頭痛になんか似てる。とはいえ、寝込むレベルではない。
  • 翌日朝にカロナールを飲んだら頭痛と腕の痛みが少しマシになった気がする。
  • あと、当日は「ふぁ〜っ」と一瞬吐き気がやってきて、「えっ、ヤバい!?」と思ったら、すぐ吐き気が去って行くという体験を2回ほどした。妻も同じような現象があったらしい。
  • これで副反応が収まっていくならありがたい。2回目もこれぐらいで済めばいいのになあ、という感想。

2回目の副反応

  • 当日は「ほんのり筋肉痛?ほんのり頭痛?」ぐらいで副反応らしい副反応はなし。
  • そのままベッドに入って寝たら「副反応に苦しめられる夢」を見た。目が覚めて「あー、夢か」と思ったけど、少し頭痛がきつくなってた。
  • 朝起きたら37.4℃で腕の痛みと頭痛が少し強かったので、6時頃にカロナール服用。9時頃に36.8℃。午前中は普通に風邪引いて微熱がある感じで、なんとか仕事ができた。
  • ただ、昼に近づくにつれて徐々に頭痛が悪化。熱もまた37℃台に。昼食後にカロナールを飲んでしばらく横になったけど、頭痛はあまり改善されず。午後の仕事を諦めてしばらく寝ることに。
  • 16時頃になって「わずかに改善した感じ」があったのと、「これ以上寝ても劇的には変わらなさそう」と思ったので、仕事に復帰(ちょっと頭がぼーっとしてたけど)。
  • 18時頃は「まだ頭痛があるけど、ピークは過ぎたかな」という感じ。そこからは徐々に体調が回復していった。
  • 2日後の朝は「ほぼ完全復活」だった。腕の痛みもほとんど引いていた。体調的には10のうち9、という感じ。
  • 1回目に体験したような「一瞬の吐き気」は今回はなかった。
  • あと、1回目も2回目も、3〜4日後に注射した周辺が少しかゆくなった。

(参考)妻の場合

  • 1回目も2回目も、僕より腕が痛そう&しんどそうだった。(妻曰く「倦怠感がすごい」とのこと)
  • 1回目も2回目も、熱はMax 37.5℃ぐらい。
  • 1回目も2回目も、翌日がしんどさのピークで、まったく動けないほどではないが、基本的に横になっていたいレベル。翌々日はちょっと倦怠感は残っているが日常生活はできるレベル。
  • 2回目の方が1回目よりも腕の痛みが強い&しんどかった、とのこと。

まとめ

ネットの情報や知り合いからは、「39℃まで上がった」とか「3日寝込んだ」みたいな話を耳にしていたので、「そこまでしんどくなったらキツいなあ」と思っていたのですが、僕や妻は幸いそこまでひどくなりませんでした。僕個人の感想としては「風邪をこじらせて微熱が出てる状態」に近かったです(特に2回目)。これぐらいであれば「翌日に仕事を休んで1日ベッドで寝ていられるなら、まあ我慢できるレベル」でした。

ただ、モデルナに比べるとファイザーの方が副反応が少ない、という噂も聞くので、モデルナだったらもうちょっとしんどくなっていたかもしれません。

いずれにしても、新型コロナワクチンの副反応は人によって大きく異なるので、あくまで「一事例」ということに留めておいてください。

人にはヒトの副反応。ということで。