ESM アジャイル事業部 開発者ブログ

永和システムマネジメント アジャイル事業部の開発者ブログです。

読みやすいテストコードについて考える

こんにちは、@kasumi8pon です。

アプリケーションの機能の追加開発をするときには、既存のテストに追加や修正を行う必要があります。また、既存のテストは現状の仕様を理解するのにも役に立ちます。そのため、テストコードが読みやすいと追加の開発がとても進めやすいです。ところが、わたしが開発しているアプリケーションのテストコードには、理解しやすく拡張もしやすいコードと、そうではないと感じるコードの両方が存在しました。よい機会なので、両者を比較しながら読みやすいテストコードについて考えてみたいと思います。なお、サンプルコードは RSpec と FactoryBot を利用して記載します。

内容を表す適切なラベルがついているか

メソッドの戻り値をそのままテストしているなど、テストの内容が自明である場合、以下のように説明を省略しても理解は容易です。

describe '#published?' do
  subject { book.published? }
  let(:book) { build(:book, published: true) }

  it { expect(subject).to be true }
end

しかし、期待する値の根拠が自明でないテストを書く場合、説明を記述するとのちの理解の助けになります。

# Bad
describe '#price' do
  subject { book.price }
  let(:book) { build(:book, price: 2_000, bargain_price: 500)

  # なんのケースをテストしてるの?
  it { expect(subject).to eq 1_500 }
end

# Good
describe '#price' do
  subject { book.price }
  let(:book) { build(:book, price: 2_000) }

  it { expect(subject).to eq 2_000 }

  context 'bargain_price がある場合' do
     let(:book) { build(:book, price: 2_000, bargain_price: 500) }

     it 'price から bargain_price の値を引かれた価格となること' do
      expect(subject).to eq 1_500
    end
  end
end

テストに説明があることで、他の箇所を参照せずともどんなテストなのかがわかるようになります。 小さなことに感じますが、仕様がわからないときにはとても理解の役に立ちます。

Feature Spec や System Spec を書く場合は、シナリオが簡潔に記載されているとわかりやすいです。これらのテストは複数の操作から成り立つことが多いため、説明がない場合ひと目でテストの内容を理解することは難しいためです。 操作部分のコードを追ってなんのテストをしているかを調べることはできますが、説明があればすぐ直感的に理解することができます。

describe '本の購入について' do
  # Bad (最後まで読まないとどんなテストかわからない)
  scenario do
    # ...
    # いろいろな操作
    # ...
    expect(page).to have_content '購入しました'
  end

  # Good (最初に何をテストしているのかがわかる)
  scenario '新発売の本の中から本を選んで購入する' do
    # ...
    # いろいろな操作
    # ...
    expect(page).to have_content '購入しました'
  end
end

RSpec には --format オプション(--format option - Command line - RSpec Core - RSpec - Relish)があり、--format documentation と指定することでテスト結果を仕様書のような形で出力することができます。このとき説明が不足していると文章として意味をなさない形になってしまいます。 --format documentation を利用したときに意味が通るようにテストを書くことは、読みやすいテストにも繋がります。

# Bad
本の購入について
  is expected to have text "購入しました"

# Good
本の購入について
  新発売の本の中から本を選んで購入する

構造が適切か

同じレベルのテストが違う階層にまたがっていたりすると、テストの全体像が把握しづらかったり、テストを追加するときに困ることがあります。以下の例では編集が新規登録時の一部の機能のように読めてしまい混乱しますし、削除の機能を追加するときにテストをどこに追加しようか迷ってしまいます。

# Bad (編集は新規登録と何か関係があるのかな?)
describe '本を新規登録する' do
  # 新規登録に関するテスト
  describe '本を編集する' do
    # 編集に関するテスト
  end
end

# Good (独立した同じレベルの機能は、同じ階層に書こう)
describe '本を新規登録する' do
  # 新規登録に関するテスト
end
describe '本を編集する' do
  # 編集に関するテスト
end

この例は極端ですが、現実のプロジェクトでは機能が多くて、テストが本来あるべき階層から飛び出してしまっているものを見かけることがあります。 このようなときも、RSpec の --format documentation で結果を出力して文章として読みづらいものがあると、不適切な階層にテストを書いていることがわかります。

また、テストデータのセットアップも適切な箇所で行うべきです。 下の例では、一見データは 20 件しか作成していないはずなのに、テスト内では 21 件目のデータについて言及しています。よくよく上の方を見ていくと、このテスト内で作成しているデータとは別でデータが作成されているようでした。

# Bad
describe '本の一覧機能について' do
  before do
    create(:book)
  end
  
  describe 'xxx' do
    # 他のテスト
  end

  describe 'ページネーションについて' do
    before do
      create_list(:book, 20)
    end
    
    # 20 冊しかつくっていないはずなのに、21 冊目はどこから出てきたのだろう?
    scenerio '21 冊目の本は 2 ページ目に表示される' do
      # 2 ページ目の表示を確認するテスト
    end
  end
end

必要な箇所のみに絞ってテストデータを生成すれば、読みやすくなります。 また、あとでテストを追加するときに認識していないデータによって想定外の状況が引き起こされることを防げます。

# Good
describe '本の一覧機能について' do
  describe 'xxx' do
    before do
      create(:book)
    end
    # 他のテスト
  end

  describe 'ページネーションについて' do
    before do
      create_list(:book, 21)
    end

    scenerio '21件目の本は 2 ページ目に表示される' do
      # 2ページ目の表示を確認するテスト
    end
  end
end

おわりに

わたしが思う読みやすいテストの特徴の一部を紹介しました。あくまでわたしが感じたことなので、別の書き方が読みやすいという方もいらっしゃると思います。テストの書き方には唯一の正解があるわけではないので当然です。しかし、唯一の正解がなかったとしても、読みやすさを意識せずに書いたコードと意識して書いたコードでは後から読み返したときに大きな違いがあるとわたしは考えています。この記事がチームメンバーや未来の自分の開発のしやすさのためにテストコードの読みやすさについて考えるきっかけになれば嬉しいです。