Edited at

システムスペックやフィーチャスペックで「〜が表示されないこと」だけを検証するのはちょっと危険、という話


はじめに

あなたは現在開発中のRailsアプリケーションでUserを削除する機能をテストしたいとします。

このアプリケーションではUser一覧画面に表示されている"Destroy"リンクをクリックすると、Userを削除できます。

Screen Shot 2019-09-23 at 18.32.31.png

  ↓

Screen Shot 2019-09-23 at 19.44.33.png

そこであなたはRSpecで次のようなテストコード(システムスペック)を書きました。


users_spec.rb

RSpec.describe "Users", type: :system do

let!(:user) { User.create! name: 'Alice' }
example 'Delete user' do
visit users_path

# 削除前には"Alice"が表示されている
expect(page).to have_content 'Alice'

# 削除を実行する
click_link 'Destroy'

# 削除が完了すると"Alice"は画面に表示されない
expect(page).not_to have_content 'Alice'
end
end


たしかに、このテストコードは一見「Userを削除すること」を正しくテストしているように見えます。

ですが、このテストコードでは本来失敗と見なすべきテストをパスさせてしまう可能性があります。

それはなぜでしょうか?

そして、このテストコードはどう改善すればいいでしょうか?


本当は失敗してるのにテストがパスしてしまう例

一般的に、システムスペックやフィーチャスペックで、not_to have_content 'Alice' のように「〜が表示されないこと」だけを検証して終わっているテストコードはちょっと危険です。

よって、上に挙げたテストコードもやはり危険です。

それはなぜか?

だって、もしかすると突然の仕様変更によって、「権限がないため、ホーム画面にリダイレクトされる」という結果に終わるかもしれないからです。

ほら、こんなふうに!

Screen Shot 2019-09-23 at 18.32.31.png

  ↓

Screen Shot 2019-09-23 at 19.25.21.png

これでも「"Alice"は画面に表示されない」という条件は満たしているので、テストはパスします。

しかし、当然ながらこのテストは本来失敗として検知されるべきです。


「〜ではないこと」は成立する条件が非常に幅広い

「〜ではないこと」は言い換えると、「〜でなければ何でも構わない」という意味になります。

つまり、「〜ではないこと」は成立する条件が非常に幅広いのです。

反対に「〜であること」は成立する条件が限定されます。


改善策=肯定的な検証も追加してテストがパスする条件を制限する

というわけで、「〜が表示されないこと」を検証する場合は、なるべく「〜は表示されていること(つまり肯定的な内容)」も一緒に検証するようにしましょう。

そうすればテストがパスする条件を限定することができます。

さきほど使ったテストコードを使って、具体的な改善例をいくつか以下に示します。


特定の文言が表示されていることを一緒に検証する

たとえば、以下は「削除完了のメッセージが表示されていること」を一緒に検証する例です。

# 削除が完了すると"Alice"は画面に表示されない

expect(page).not_to have_content 'Alice'

# 加えて、削除完了のメッセージが表示されている
expect(page).to have_content 'User was successfully destroyed.'


現在のURL(current_path)を一緒に検証する

他にも、current_pathを検証するのもひとつの手です。

# 削除が完了すると"Alice"は画面に表示されない

expect(page).not_to have_content 'Alice'

# なおかつ、User一覧画面を表示している
expect(current_path).to eq users_path


DB上からもデータが削除されたことを検証する

また、今回のサンプルコードのようにデータを削除するテストであれば、モデルの件数が変わることもあわせてテストしておくと、さらにテストの堅牢性が増します。

# DestroyリンクをクリックするとUserの件数が1件減る

expect {
click_link 'Destroy'
}.to change(User, :count).by(-1)

このようなテストコードになっていれば、「権限が無いため、ホーム画面にリダイレクトされた」という結果は「テストの失敗」として検知することができます。


まとめ

ここで挙げたサンプルコードはあくまでひとつの例なので、必ずしもみなさんのテストコードにも同じように適用できるとは限りません。

実際にどういうテストコードを書くとテストの失敗を正しく検知できるのかは、アプリケーションの要件によって異なります。

少なくともみなさんに意識しておいてほしいのは、「〜が表示されないこと」だけを検証して満足しているテストコードは、本来失敗と見なすべきテストをパスさせてしまう恐れがある、ということです。

「〜が表示されないこと」だけを検証すると、どういうケースで失敗の検知漏れが発生するのか、そして、どういう検証と組み合わせれば確実に失敗を検知できるのか、テストコードをコミットする前に一度じっくり考えてみてください😉