よく見る Capybara を利用したコードの誤った書き方

Ruby

Capybara は良くクローラーとか E2Eテストで使われると思いますが、比較的E2Eテストで利用されることが多いと思います.

ここでは E2Eテスト での Capybara の誤ったコードを紹介していきます.

動的なDOM

現在ではJSによって動的にDOMが変更されるため、常にDOMに変更が加わることを前提にコードを書く必要があります.

Capybara APIの Check系のメソッドには対象の状態(DOM要素の存在、表示の状態)になるまでリトライされる機構があります.

この辺の非同期の内容は公式ドキュメントのAsynchronous JavaScript (Ajax and friends)に書いてあります.

反対にCheck系以外の値を単純に取得する様なメソッドは、その時点のDOMから情報を取得します.

動的なDOMをテストするには以下のような3つステップを踏むことで対応ができます.

  1. 事前状態のチェック
  2. アクション
  3. 事後状態のチェック

例えば、ユーザ名の変更のユースケースを例にすると以下のように表現できる.

  1. 事前状態のチェック
    expect(page).have_field('firstName', with: "old name")
  2. アクション
    find("input.firstName").set("new name")
    find("button.update").click
  3. 事後状態のチェック
    expect(page).have_field('firstName', with: "new name")

input要素 には name属性 指定がない

Input要素 には has_field? を使う.
以下のDOMを前提に説明していきます.

<div class="firstName">
  <input name="firstName" value="John" />
</div>
<div class="lastName">
  <input value="Smith" />
</div>

input要素のname属性がfirstName の入力値を確認する場合は以下の様にします.

find(page('.firstName')).has_field?('firstName', with: 'John') # => true

input要素のname属性がない場合は以下の様に書くことになるので直感的ではない為、避けるべきでしょう.

find(page('.lastName')).has_field?(nil, with: 'Smith') # => true

配列として扱う

# bad
expect(all('.title').count).to eq 5
expect(all('.title')[1].text).to eql('hello')


# good
expect(page).to have_selector('.title', count: 5)
expect(page).to have_content('.title', with:'hello', exact: true)

ノードの数を調べる場合は has_selector?count を検討するべきです.

n番目のノードを取得する際は :nth-child():nth-of-type()を検討すると良い.

all('.title').countall('.title')[1].text が相応しくない理由は、評価時点でのDOMしか確認できないからです.

count の場合は要素の数を取得しあた後にDOMに変更があった場合で期待値と異なってしまいます.

[1].text の場合は2つの問題があり、1つ目は [1] でもしDOMが1つの要素の配列だった場合は nil になってしまい nil.text の様な評価をされてしまいNoMethodError が起きます. 2つ目は例え要素が2つ以上存在して取得できた場合でも、count同様その時点のDOMの評価になるため期待値と異なってしまう場合があります.

値を取得してしまう

has_content?has_text? のエイリアスで同じものです.
オプションの exact のデフォルトは false なので正確に一致させる場合は true を指定する方が良いでしょう. false の場合は部分一致になります.

# bad
expect(find('.title').text).to eql 'hello'

# good
expect(page).to have_content('.title', with: 'hello', exact: true)
タイトルとURLをコピーしました