give IT a try

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

【動画あり】続・リーダブルテストコード:みなさんからの質問に答えてみました #vstat

前回書いたブログの続きです。

blog.jnito.com

「VeriServe Test Automation Talk No.3」というオンラインイベントで登壇した際に参加者のみなさんから質問をたくさんいただきました。

一部はイベント内で回答したのですが、時間内に全部回答することはできなかったので、ここで回答することにします。
ただし、テキストで回答を書こうとするとかなり大変なので、YouTube動画にしています。

興味深い質問が多数あって、何かしらみなさんの参考になると思うのでぜひ一度ご覧ください😄


www.youtube.com

動画を見る時間がない、という人のために、ざっくりとQ&Aの内容を書いておきますね。

質問1

先日開発が始まって半年くらいのあるプロダクトの開発を引き継ぎました。
ドキュメントが一切なく開発者が私一人です。
テストを作り始めていきたいと思うのですが、まず着手するべことに
ついてアドバイスいただけないでしょうか。

レガシーな巨大リポジトリにテストを追加しようと思って二の足を踏んで
いるのですが、どういったファイルからテストを書いていますか?
また、どうすればレガシーコードにテストを導入しやすいでしょうか?
目的や優先度、テストの書き方など、考慮すべきポイントがあれば教えて
下さい。

ユースケース的に一番重要なE2Eテストから書いていきましょう!

質問2

レビューをする際、機能自体のレビューにかけた時間に対してテストの
レビューにかける時間はどのくらいの割合で行っていますか?

時間的な割合だと10〜30%ですかね〜。

質問3

初心者な質問かと思うのですが、失礼致します。
テストを書いた時に、テストのコード量に応じて仕様変更に対して動きが
重くなると思いますが (テストを沢山直さないといけない)
これは仕方がない事なのでしょうか?何か工夫はありますか?

基本的には仕方ないことですね。
工夫としては「権限だけをテストするテストコード」と「それ以外のテストコード」を分割することがあります。

あと、このツイートも追記しておきます……。

質問4

たとえばバックエンドのAPIを開発する際、エンドポイントごとのテストは
必ず書くのですが それより奥の層(サービスメソッドやリポジトリメソッド)
は必要に応じて、とすることが多いです
本来的にはその奥の層の各部品に対しても、基本的なユニットテストを
整備してあげるのが望ましいとは思うのですが、
優先度としては上記のようにする場合が多いです
みなさんはそのあたりどう考えていらっしゃるでしょうか

その考え方で問題ないと思います!

質問5

RSpecで、例えばuserのroleがadminである、ことがテストの結果として
必要な場合、 let(:user) {create(:user, role: :admin)} のような形で描くか、
before do ... enduser.update(role: :admin)のようにかくか、
あるいはit do ... endの中で

user.update(...)
expect(...)

と書くか、どれが良いでしょうか?
個人的にはbeforeやitの中がわかりやすい気がしていますが、let内で書くのが
今担当しているPJ 内の慣習のようになっているので気になっています。

「テストの結果」ではなくて「テストの事前条件」のことでしょうか?
であればletやbeforeで書くことが多いです。
可読性が一定レベルを超えていれば、どちらでも良いと思います。
テストコードについて、あまり細かいレベルであれを直せ、これを直せというのは時間の使い方がもったいないかも。

質問6

仕様Aから仕様Bに変更した場合、テストはどう変更すると良いかの方針
などありますか? 既存の「仕様Aであること」を確認するテストを
「仕様Bであること」のテストに書き換えるのか、
「仕様Aでないこと」と「仕様Bであること」のテストが別にあった方が
いいのか、など

「仕様Bであること」のテストだけでOKです。
ただ、「仕様Aでないこと」もテストしないと不安になるときは「仕様Aでないこと」のテストも書きます。

質問7

テストコードを再利用したい時に、振舞いとデータに分けて実装したく
なります。 そうなる変数を使いたくなります。
この様な場合、どう対処したら良いでしょうか?

パラメタライズドテスト(データ駆動テスト)を利用するといいかもしれません。
下記ブログ記事の後半にパラメタライズドテストの利用例が載っています。

blog.jnito.com

質問8

変数を使うと、テストコードを変更がいらないというメリットもあると思うのですが、いかがですか?

class Hoge
  NAME = 'suzuki'
  def name
    NAME
  end
end
# 名前が変わったときに変更しないといけない 
it 'NAMEがsuzukiであること' do
  expect(Hoge.name).to eq 'suzuki' 
end
# 名前が変わっても変更しなくていい 
it 'NAMEがtanakaであること' do
  expect(Hoge.name).to eq Hoge.NAME 
end

アプリケーション側の定数は直接参照しない方がいいです!
詳しくはこちらのブログをご覧ください。

blog.jnito.com

質問9

こんばんは。Ruby初学者で、Railsを使ってアプリケーション開発の
勉強をしている者です。
質問なのですが、初学者が陥りがちなNGテストコードあるある等が
あれば教えていただければ幸 いです。

初学者が陥りがちなNGテストコードあるあるといえば、idをベタ書きする人をたまに見かけますね。
「ベタ書き大事」といってもidをベタ書きするのはNGです。これは自動採番される値なので。

# idはベタ書きしちゃダメ!
let(:user) { FactoryBot.create(:user, id: 1) }
let(:project) {
  FactoryBot.create(:project,
    id: 1,
    name: "RSpec tutorial",
    user_id: 1)
}
let!(:task) { FactoryBot.create(:task, project_id: 1, name: "Finish RSpec tutorial") }

Railsであればモデルの関連をうまく活用すればidをベタ書きせずに済みます。

# 関連を活用すればidをベタ書きする必要はない
let(:user) { FactoryBot.create(:user) }
let(:project) {
  FactoryBot.create(:project,
    name: "RSpec tutorial",
    user: user)
}
let!(:task) {
  project.tasks.create(name: "....")
}

ちなみに上記のコードは「Everyday Rails - RSpecによるRailsテスト入門」のサンプルコードを一部改変したものです。

leanpub.com

質問10

システムテストを統合テストより先に書くべきでしょうか??
Railsのminitestを使っていて、最もエンドユーザの行動に近いブラウザを
使ったシステムテスト を先に書き、時間があればintegrationやcontroller,
modelのテストを後に書くようにしていま す。

システムテスト(E2Eテスト)が優先、で良いと思います。
僕がテストを書くときもシステムテストかモデルのテスト(システムスペックかモデルスペック)が9割以上です。

質問11

カバレッジについての指針は何かありますか

カバレッジ率については80%以上を目指すようにしています。
100%を狙うのは費用対効果があまりよくないので無理に目指さなくて良いです。
大きな機能をリリースする前は具体的にどのコードがテストされていないのかをレビューすることもあります。

まとめ

僕が回答した質問は以上です。詳しい内容はこちらの動画をどうぞ〜!


www.youtube.com

あわせて読みたい

勉強会本編の登壇内容についてはこちらのブログにまとめてあります。
blog.jnito.com

同じイベントに登壇していた風間さんもいくつか同じ質問について回答してくれているので、こちらも参考になると思います!
nihonbuson.hatenadiary.jp

過度なDRYは読みやすさの敵!?「リーダブルテストコード」という発表をしました #vstat

先日、このブログでもお伝えしましたが、「VeriServe Test Automation Talk No.3」というオンラインイベントで登壇してきました。

veriserve-event.connpass.com

申込者数はなんと1000人を超えていて、大変驚きました。

僕は「リーダブルテストコード」というテーマで発表しました。スライドはこちらです。

Twitterでたくさんシェアされたり、はてなブックマークがたくさん付いたり、こちらもすごい反響でビックリしました。

で、どんな内容だったの?

ひとことで言うなら「テストコードを徹底的にDRYにしようとしちゃダメよ!」というお話です。
このネタは昔からQiitaやTwitterとかでことあるごとに話してきましたが、この勉強会であらためてなぜダメなのか、DRYに書かず、どう書くべきなのか、という話を力説してみました。

優秀なプログラマほど、「DRYが善」と信じて止まないので、テストコードに対してもDRYを追求しようとしてしまいがちです。
今までいろんな人のテストコードをレビューしてきましたが、僕が初めてコードレビューした人の9割ぐらいがテストコードをDRYに書こうとしていたように思います。
まあ、「コードを見たらDRYにしたくなる」というのは、ある意味プログラマの職業病みたいなものなので仕方ない面もあるのですが。

とはいえ、それだといつまでたっても読みやすいテストコードは書けません。
読みやすいテストコードを書くためには、「あえてDRYを捨てる」という発想の転換が必要です。

こういう考え方が世の中のプログラマにもっと広がってほしいな〜と思って、今回のような発表内容に至りました。

2022/9/5追記:動画出ました!

当日の登壇動画が公開されました。当日見逃した人やもう一度見たい、という人はこちらをどうぞ!
blog.jnito.com

2022/10/7追記:書き起こし記事も出ました

ログミーさんによる書き起こし記事も公開されました。こちらもあわせてどうぞ!
logmi.jp

他の登壇者のみなさんのスライド

僕の他にも末村 拓也さんと風間 裕也さんが、同じく「リーダブルなテストコード」について、非常にためになる発表をされていました。
こちらもぜひご覧ください。


質疑応答&パネルトークで話した内容

勉強会の後半は登壇者3人とモデレータによる質疑応答&パネルトークでした。
いろんなQ&Aのネタがあったのですが、その中から1つだけ僕が話したトピックをここに書いておきます。

Q. アプリケーション側に定数があった場合、その定数をテストコード側でそのまま利用しても良いか?

定数にもいろんな定数がありますが、ここでは一例として消費税の税率を定数化していた場合を考えてみます。

たとえばこんな感じですね。

class Product
  TAX_RATE = 0.1
  # ...
end

テストコードは以下のようにProductクラスで定義した定数を参照しています。

example '税込み価格が返る' do
  product.price = 1000
  expect(product.price_including_tax).to eq \
    product.price * (1 + Product::TAX_RATE)
end

このテストコードを評価する場合、良い・悪いでどちらかひとつ選べと言われたら、僕は「悪い」を選択します。
僕だったらテストコードはこんなふうに税込み価格をベタ書きします。

example '税込み価格が返る' do
  product.price = 1000
  expect(product.price_including_tax).to eq 1100
end
「なぜ悪いの?」

テストコード内で定数Product::TAX_RATEを参照した場合、将来もし消費税率が15%に変わっても定数の値を変えるだけで修正が終わります。テストコードには何も手を加える必要がありません。

一見、これはDRYであることのメリットのように思えますが、実はデメリットになる可能性も大いにあります。

アプリケーション側の仕様変更に合わせてテストコードが仲良く二人三脚で歩いて行くようなコードを書くと、アプリケーション側の実装にバグが埋め込まれた場合にもテストがパスしてしまう可能性があります。つまり、バグを検知すべきテストコードがやすやすとバグを見逃す可能性があるわけです。これは致命的な問題です!

class Product
  # 寝ぼけたプログラマが消費税率15%をそのまま15としてしまった!
  TAX_RATE = 15
  # ...
end
example '税込み価格が返る' do
  product.price = 1000
  # 税込み価格が1万6000円になってるのにテストがパスしてしまう!!
  expect(product.price_including_tax).to eq \
    product.price * (1 + Product::TAX_RATE)
end
ベタ書きした方がバグを検知しやすい

一方、expect(product.price_including_tax).to eq 1100のようにベタ書きしてあれば、定数を変更したときにテストが失敗します。
そして、次のようなテストコードに書き替えれば、TAX_RATE = 15という設定値が間違いであることにも気付くことができます。

example '税込み価格が返る' do
  product.price = 1000
  # TAX_RATE = 0.15 でなければこのテストは失敗する
  expect(product.price_including_tax).to eq 1150
end

こうした理由から、テストコード内でアプリケーション側の定数を参照するのは避けた方が良い、と考えることができます。

「でも、DRYにしないとテストコードの修正が大変なんです!」

もちろん、テストコードはDRYではなくなるため、仕様変更が発生すると大量のテストが失敗することもあります。ただ、それはデメリットではなく、仕様変更のインパクトがテストコードのおかげで可視化された、と好意的に捉えるようにしましょう。

ただし、「これはある程度DRYにしておかないと、今後のテストコードの保守がかなりキツい」という場合は「ほどほどのDRY」は許容しても構いません。
そういうケースでは僕はrspec-parameterizedというgemを使って、パラメタライズドテストを書くことが多いです。

github.com

Twitter上のみなさんの反応など

ツイートを見る限り、おおむね好評だったようでほっとしました😄

なお、当日の参加者のみなさんのツイートはこちらにまとめられています。
togetter.com

おまけ

いつものことなのですが、登壇前には何度もリハーサルして時間配分やトーク内容を洗練させていっております。

僕が登壇前にどんな準備をしているのか知りたい方は、こちらのエントリをご覧ください。
blog.jnito.com

まとめ

というわけで、このエントリでは先日登壇した「VeriServe Test Automation Talk No.3」というオンラインイベントの登壇内容を紹介してみました。
もっと読みやすいテストコードを書きたい!という人はぜひ今回の登壇内容を参考にしてみてください。

最後に、このイベントに参加してくださったみなさん、登壇者のみなさん、そして運営者のみなさん、どうもありがとうございました!
久々のオンライン登壇でしたが、みなさんのおかげで楽しく発表することができました😄

あわせて読みたい

勉強会当日にいただいた質問に一通り答えてみました。こちらもあわせてどうぞ!

blog.jnito.com

PR:RSpecの本とか、Rubyの本とか

この講演を聞いてテストコードに俄然興味が沸いてきた!という人は、ぜひ「Everyday Rails - RSpecによるRailsテスト入門」をどうぞ。
RSpecを使った実践的なテストコードの書き方が学べます。
leanpub.com

また、「プロを目指す人のためのRuby入門」でも例題を解く際にテスト駆動開発のスタイルを取り入れてます(使用しているテスティングフレームワークはminitestです)。
Rubyと同時にTDDを学びたい方はこちらもぜひ!

【プログラミング初心者向け】クラスメソッドとインスタンスメソッドはどう使い分けるべき?

はじめに

ruby-jpのSlackで以下のような質問が投稿されていました。

クラスメソッドとインスタンスメソッドの具体的な違いがわかりません。
現状「クラスメソッドはクラスから実行でき全体に関する処理を書くときによく使うもの。インスタンスメソッドはインスタンスから実行でき、個別具体的な処理を書くときに使うもの。」という理解をしています。そして実装の際に「これはクラスメソッドとインスタンスメソッドどちらで書くべきなのか」悩むケースが多いです。
上記を踏まえて質問です。

  • クラスメソッドとインスタンスメソッドの具体的な違いを皆さんはどのように定義しているか
  • どこからがクラスメソッドでどこからがインスタンスメソッドなのかの境目はどのあたりにあるか

をお伺いしたいです!

クラスメソッドとインスタンスメソッドの使い分けは僕がメンターをやっているフィヨルドブートキャンプでもよく見かける質問です。

そこで僕が考えるクラスメソッドとインスタンスメソッドの使い分けを次のように説明してみました。

なお、これはプログラミング歴があまり長くなく、オブジェクト指向プログラミングも最近勉強し始めたばかり、という初心者さんを対象にしています。

クラスメソッドとインスタンスメソッドの使い分け

初心者向けのざっくりとした指針を示すなら、「クラスメソッドとインスタンスメソッドの使い分けに迷ったら、とりあえずインスタンスメソッドにしておけ」と答えます。

その上で、「もしインスタンスを引数なしでnewした直後に何かメソッドを呼び出している場合は、それはクラスメソッドの候補かもしれない」と考えると良いです。

たとえばこんな感じですね。

# 引数なしでnewした直後にメソッドを呼んでいる
Hoge.new.do_something

# このコードもやってることは上と同じ
hoge = Hoge.new
hoge.do_somehting

# do_somethingは実はクラスメソッドが適しているかも(100%必ず、というわけではない)
Hoge.do_somehting

このとき、do_somethingがインスタンス変数(@nameなど)や、Railsならモデルの属性(emailメソッドなど)に依存している(=読み書きしている)場合はインスタンスメソッドのままにすべきですが、そうでなければクラスメソッドの方が向いています。

もちろん、細かい話をすると「ケースバイケースなので、まずコードを見せてください」となるのですが、オブジェクト指向初心者さんなら上のような指針で大きく外さないんじゃないかと考えています。

上の説明のもう少し具体的な例

以下は「クラスメソッドでいいのでは?」と思われるメソッドの例です。

calculator = Calculator.new
calculator.add(2, 3) #=> 5
calculator.add(4, 5) #=> 9

class Calculator
  def add(a, b)
    a + b
  end
end

インスタンス変数を全く使ってないので、この場合は次のようにクラスメソッドにしても問題ないですね。

Calculator.add(2, 3) #=> 5
Calculator.add(4, 5) #=> 9

class Calculator
  def self.add(a, b)
    a + b
  end
end

一方、次の場合はどうでしょうか?

calculator = Calculator.new
calculator.add(2, 3) #=> 5
calculator.add(4, 5) #=> 9
# これまでの計算履歴を返す
calculator.history   #=> ["2 + 3", "4 + 5"]

class Calculator
  attr_reader :history

  def initialize
    @history = []
  end

  def add(a, b)
    @history << "#{a} + #{b}"
    a + b
  end
end

これは履歴をインスタンス変数にため込んでいくのでインスタンスメソッドのままにしておく必要があります。
(クラスメソッドとクラスインスタンス変数にすれば・・・という議論もできますが、ややこしいのでここでは無視します)

クラスメソッドが向いているパターン3つ

さらに、RubyやRailsで実際に存在しているクラスメソッド(特異メソッド)を参考にして、「こういうメソッドはクラスメソッド向きであることが多い」というパターン3つ挙げてみます。

いずれも「インスタンスメソッドで定義するとなんか不自然」と思うようなものばかりなので、とりあえずインスタンスメソッドで作ってみて、違和感を感じたらクラスメソッド化を検討する、というようなアプローチでも良いかもしれませんね。

パターン1:関数的なメソッド

ここでいう関数的とは、数学のy = f(x)を満たすようなメソッド、つまり、入力値xに対して毎回必ずyを戻り値として返す特性を指します。

たとえばDate.valid_date?などが関数的なメソッドに該当します。

# 日付として有効であればtrue、そうでなければfalseを返す
Date.valid_date?(2022, 6, 30)  #=> true
Date.valid_date?(2022, 6, 31)  #=> false

もしこれがインスタンスメソッドになっていると不自然に思いませんか?

# dateは今日の日付を表すインスタンス
date = Date.today
# なんで「今日の日付」が他の日付の妥当性を検証しなきゃいけないの?
date.valid_date?(2022, 6, 30)
date.valid_date?(2022, 6, 31)

パターン2:新しいインスタンスを生成するメソッド

新しいインスタンスを生成する場合、そもそも「インスタンスがない状態」から出発しなきゃいけないので、自ずとクラスメソッドになります。

# 文字列をパースしてDateオブジェクトを生成する
Date.parse("2022-06-30") #=> #<Date: 2022-06-30 ((2459761j,0s,0n),+0s,2299161j)>

もしこれがインスタンスメソッドになっていると不自然に思いませんか?

# dateは今日の日付を表すインスタンス
date = Date.today
# なんで「今日の日付」が他の日付を生成しなきゃいけないの?
date.parse("2022-06-30")

ちなみにnewメソッドも「新しいインスタンスを生成するクラスメソッド」と考えることができますね。

# newも無から新しいインスタンスを生成するクラスメソッド
Date.new #=> #<Date: -4712-01-01 ((0j,0s,0n),+0s,2299161j)>

Railsのfindメソッドやscopeなんかもその一種だと考えられます。

# DBにアクセスして新しいインスタンスを生成(復元)する
User.find(id) #=> (指定したidのDB上のUserが返る)
User.all      #=> (DB上の全Userが返る)
User.active   #=> (activeなDB上のUserが返る)

パターン3:そのクラスの複数のインスタンスを扱うメソッド

これはRubyやRailsのメソッドで該当するものが見当たらなかったので、架空のメソッドで説明します。

# carsのデータをExcelファイルに出力する(架空のメソッド)
Car.export_as_xlsx(cars)

もしこれがインスタンスメソッドになっていると不自然に思いませんか?

# carは「ぷりうす」を表すインスタンス
car = Car.new('ぷりうす')
# なんで「ぷりうす」が他のcarのデータをExcel出力しなきゃいけないの?
car.export_as_xlsx(other_cars)

このほかにも「DSLのように振る舞うクラスメソッド(例: Railsの belongs_tohas_many 等)」もありますが、初心者が自分で定義することは少ない&インスタンスメソッドにするのがそもそも困難だと思うのでここでは割愛します。

RubyやRailsのクラスメソッドを分析すると勉強になるかも

今回はRubyやRailsが提供しているメソッドを中心に取り上げましたが、僕が業務でクラスメソッドを定義する場合もだいたいこのパターンのいずれかに合致しているように思います。

今回やったようにRubyやRailsのAPIドキュメントを眺めて、どういう目的でクラスメソッドが定義されているのかを自分なりに分析したりすると、クラスメソッドの使いどころがだんだんとわかってくるかもしれません。

まとめ

というわけで、今回は僕が考えるクラスメソッドとインスタンスメソッドの使い分けについて説明してみました。

オブジェクト指向的に適切なクラス設計がされていれば、クラスメソッドが登場する回数はインスタンスメソッドに比べてずっと少ないはずです。

一方、オブジェクト指向らしくないクラス設計だと、「クラスメソッドでもインスタンスメソッドでもどっちでも構わない」というメソッドが増えやすくなります。特に、オブジェクト指向に不慣れな人が作ったクラスは、すべてクラスメソッドに置き換え可能(関数的メソッドばかり)というケースもよくありますね。

ただ、このあたりの考え方はなかなか独学で身に付けるのはなかなか難しいので、先輩プログラマにコードレビューを受けたりしながら学ぶのが一番よかったりします。僕自身も初心者だった頃は先輩プログラマの指導を受けながらオブジェクト指向の考え方を学びました。

なので、身近に頼れる先輩がいる初心者さんは、積極的にコードレビューを依頼していきましょう!

あわせて読みたい

かなり昔に書いた記事で対象言語もJavaが中心ですが、僕が昔読んだオブジェクト指向関連の技術書を以下のエントリにまとめています。

blog.jnito.com

あと、こちらも古い記事ですが、オブジェクト指向関連で特に役に立った書籍を数冊ピックアップして紹介しています。

blog.jnito.com

【PR】フィヨルドブートキャンプならコードレビューしてもらえるよ

僕がメンターをやっているフィヨルドブートキャンプにはオブジェクト指向プログラミングのプラクティスも用意されています。

学習内容 | FJORD BOOT CAMP(フィヨルドブートキャンプ)

僕を含む経験豊富なメンターが受講生のみなさんが書いたプログラムをしっかりコードレビューするので、身近に頼れる先輩がいない場合はフィヨルドブートキャンプで学習することも検討してみてください😃

bootcamp.fjord.jp

【PRその2】「プロを目指す人のためのRuby入門」もよろしくお願いします

オブジェクト指向を専門に扱う本ではありませんが、拙著「プロを目指す人のためのRuby入門(通称・チェリー本)」でも自作クラスの作り方を丁寧に説明しています。

そもそもクラスを定義する構文やRubyの言語機能がよくわかっていない、という方はぜひ「プロを目指す人のためのRuby入門」でRubyについて学んでみてください!