かもメモ

自分の落ちた落とし穴に何度も落ちる人のメモ帳

Ruby メソッド定義 (def式) 内でメソッド定義 (def式)した場合のメモ

Rubyのお勉強をしていてメソッド定義内でメソッド定義したら不思議な挙動 (JS脳には予想外だっった) だったのでメモ。

メソッド定義 (def式) 内に メソッド定義 (def式) をした場合

def a
  puts 'A'
  def b
    puts 'B'
  end
  print "self.b > "
  self.b
end

a
# => "A"
# => "self.b > B"
b
# => "B"

えっ? def a 内で定義した def bdef a の外からでも呼び出せるの…!?
JS脳的には下記のようなイメージだったのでびっくりしました

function a() {
  console.log('a')
  function b () {
    console.log('b')
  }
  b()
}
a()
// a
// b
b()
// Error: Uncaught ReferenceError: b is not defined

Ruby のメソッド定義 (def式) は呼び出されるとトップレベルに定義される?

Ruby では def式 の書かれている場所を通れば、そのファイル?のトップレベルにメソッドが定義されるのかなという印象を持ちました。

def a
  puts 'A'
  def b
    puts 'B'
  end
end

a # => "A"
b # => "B"

a を呼び出してないと b は未定義になる

def a
  puts 'A'
  def b
    puts 'B'
  end
end
b
# => undefined local variable or method `b` for main:Object

Ruby は関数ではなくメソッドで、レシーバー.関数名 引数 の形で呼び出されるとあったので、def式で定義すると、そのファイルのトップレベルのオブジェクトがレシーバーになるからトップレベルから呼び出せるのかな〜?とか考えていました。詳しくはないので完全に憶測なのですが…

メソッド定義 (def式) は後から呼び出されたもので上書きされる?

先の実験でメソッド定義内にあるメソッド定義は外側のメソッドを呼び出した際に定義されることがわかりました。
という事は同じ名前のメソッドが外側にもある場合、メソッドの呼び出し順によって、呼び出されるメソッド上書きされが変わってしまう可能性があるのではないかと思い試してみました。

def a
  puts 'A'
  b
  def b
    puts 'B inside A'
  end
  b
end

def b
  puts 'B'
end

b # => "B" … トップレベルのメソッドが呼び出される

a
# => "A"
# => "B" … メソッド内の def b の定義を通る前なので、外側のトップレベルのメソッドが呼び出されている
# => "B inside A" …  a 内の b メソッドが定義され、新しく定義された b が呼び出されている

b # => "B inside A"
# トップレベルの b メソッドは a 内で定義された方に上書きされてしまっている

予想通りでした。
Ruby の場合同じファイル内でメソッド定義する場合はメソッド名が重複しないように気をつける必要がありそうです。
そもそもトップレベルにつらつら定義するのが Ruby 的お作法では良くないのかも? この辺りの文化については詳しくないのでどこで学べば良いのでしょう…?

ポエム

Ruby のメソッド定義内でメソッド定義したらメソッド外で呼び出したらエラーになるよね〜って思って実行したらふつーに呼び出せてしまってびっくりしました。慣れ親しんでる JavaScript の関数定義と違ってRuby の メソッドて定義 (def式) は常にトップレベルに定義されるって感じっぽいのですね。JavaScript感覚で使ってるとハマりそうでした。

Ruby は関数じゃなくてメソッドって呼ぶのなんでだろう?慣習???みたいに思ってたのですが、今回のハマりで読んでたRubyの本

レシーバーに含まれる情報を使用しないメソッドを関数的メソッドと呼ぶ。全てのメソッドはレシーバー情報を持っているので純粋な関数ではない
初めてのRuby 著 Yugui

みたいな感じに書かれていて、オブジェクトに紐付いてて呼び出し元(レシーバー)の情報を持っているとメソッド?
トップレベルに置かれた def式 は mail オブジェクトとかに紐付いててその情報持ってるってこと??
って事は JavaScript はブラウザで実行したらfunction も全部 window オブジェクトに紐づいて window.func() で呼び出せるから関数じゃなくて本当はメソッドなん???
むむむ?????
と解ったよーな、解らんよーな感じになりました。
ナルホドわからん!! 俺は雰囲気でぷろぐらむを描いている!!!!


初めてのRuby

初めてのRuby

プログラミング言語 Ruby

プログラミング言語 Ruby

RSpec Capybara 非表示な要素のテストにハマる

RSpec でテストをしていて、非表示にしてある要素が取れずにハマった。

Capybara さんはデフォルトでは画面上に見える要素のみを検索するので display: nonehidden になっている要素を触るできないっぽい。

<div class="modal" style="display:none"></div>
<input type="hidden" id="secret_val" value="XXX">
scenario 'モーダルが存在する' do
  expect(page).to have_selector('.modal')
end

scenario '#secret_val の値は XXX' do
  expect( find('#secret_val').value ).to eq 'XXX'
end

画面に見えてない要素は触れないのでテストが通らない

visible: false オプションを使う

scenario 'モーダルが存在する' do
  expect(page).to have_selector('.modal', visible: false)
end

scenario '#secret_val の値は XXX' do
  expect( find('#secret_val', visible: false).value ).to eq 'XXX'
end

👉 success
オプションを付ければ非表示要素も検索してくれるのでテストが可能になる!

Options Hash (**options):
visible (Boolean, Symbol) — Only find elements with the specified visibility:

  • true - only finds visible elements.
  • false - finds invisible and visible elements.
  • :all - same as false; finds visible and invisible elements.
  • :hidden - only finds invisible elements.
  • :visible - same as true; only finds visible elements.

cf. Module: Capybara::Node::Finders — Documentation for jnicklas/capybara (master)

非表示になっている要素のコンテンツは have_content / have_text で取れない

例えばデフォルトで非表示になっているモーダルのタイトルの内容があっているかテストしたいような場合

<div class="modal" style="display:none">
  <p class="modal_title">アイカツを見ろ</p>
</div>
scenario '正しいモーダルが出力されていること' do
  expect( find('.modal', visible: false) ).to have_content 'アイカツを見ろ'
end

👇

Failure/Error: expect( find('.modal', visible: false) ).to have_content 'アイカツを見ろ'
   expected to find text "アイカツを見ろ" in "". (However, it was found 1 time including non-visible text.)

非表示の要素は取れていても have_contenthave_text は 空 ("") と判断されてテストが落ちてしまう

have_selectortext: オプションも使ってマッチさせることでコンテンツ内容のテストができる

但し、非表示要素内の要素は have_selector でも visible オプションが必要

非表示なコンテナ内にあって、直接 display: none などのスタイルを当てられててない要素でも visible オプションがないと要素を取得できずにテストが落ちてしまう

scenario '正しいモーダルが出力されていること' do
  expect( find('.modal', visible: false) ).to have_selector('.modal_title', text: 'アイカツを見ろ')
end

👇

Failure/Error: expect( find('.modal', visible: false) ).to have_selector('.modal_title', text: 'アイカツを見ろ')
   expected to find visible css ".modal_title" with text "アイカツを見ろ" within #<Capybara::Node::Element tag="div" path="/HTML/BODY[1]/DIV[2]/DIV[2]/DIV[2]/DIV[1]"> but there were no matches. Also found "", which matched the selector but not all filters.

have_selector にも visible オプションを付ける

scenario '正しいモーダルが出力されていること' do
  expect( find('.modal', visible: false) ).to have_selector('.modal_title', visible: false, text: 'アイカツを見ろ')
end

👉 success
これならテストが通る!
ただ感覚的に「◯◯のテキスト が 'XXXX'」となってる方が直感的なわかり易さもテストの構造としても良い気がするので微妙な感じ…

.text(:all) で非表示になっているテキストノードを取得できる

#text(type = nil, normalize_ws: false) ⇒ String
Retrieve the text of the element. If ignore_hidden_elements is true, which it is by default, then this will return only text which is visible. The exact semantics of this may differ between drivers, but generally any text within elements with display:none is ignored. This behaviour can be overridden by passing :all to this method.
cf. Method: Capybara::Node::Element#text — Documentation for jnicklas/capybara (master)

text(:all) でテキストを取得したほうが「◯◯のテキスト が 'XXXX'」と書けるので非表示になっている要素の内容のテストには良さそう

<div class="modal" style="display:none">
  <p class="modal_title">アイカツを見ろ</p>
  <div class="modal_body">
    <strong>アイカツオンパレード始まったから絶対見て!!!!</strong>
  </div>
</div>
scenario '正しいモーダルが出力されていること' do
  # 文字列に含まれるをテストするので include を使う
  expect( find('.modal', visible: false).text(:all) ).to include 'アイカツを見ろ'
end

👉 success
テストが通りました ٩(ˊᗜˋ*)و

非表示の要素内のテキストは find()visible オプション・ text():all 引数の両方がないとダメ

text:all がないと空("")になるのでテストが通らない

scenario '正しいモーダルが出力されていること' do
  expect( find('.modal', visible: false).text ).to include 'アイカツを見ろ'
end

👇

Failure/Error: expect( find('.modal', visible: false).text ).to include 'アイカツを見ろ'
   expected "" to include "アイカツを見ろ"

find()visible オプションがないとそもそも要素を取得できないのでテキストも取得できない

scenario '正しいモーダルが出力されていること' do
  expect( find('.modal').text(:all) ).to include 'アイカツを見ろ'
end

👇

Failure/Error: expect( find('.modal').text(:all) ).to include 'アイカツを見ろ'
  Capybara::ElementNotFound:
    Unable to find visible css ".modal"
text の引数はこんな感じになっているそうです

見えないテキスト(display:none)が text メソッドの戻り値に含まれるかどうかについて
2.1 では、text メソッドの引数によって挙動を切り替えることもできる

find("#thing").text           # Capybara.ignore_hidden_elements によって挙動が変わる
find("#thing").text(:all)     # 見えないテキストも含む
find("#thing").text(:visible) # 見えるテキストだけ
cf. capybara 2.1 を学ぶ - おもしろwebサービス開発日記

感想

  • 非表示になっている要素には visible: false オプションを使う
  • 非表示になっている要素の内容をテストするには find(selector, visible: false).text(:all) を使う

のが良さそうです。
 
RSpec も Capybara も何も分からん… (docker-mac でのテスト走らせるのが遅くて辛いからどうにかなってほしぃ…


[参考]

カピバラさん PCクッション

カピバラさん PCクッション

RSpec Capybara href の無い a タグにハマる

RSpecの feature spec で href の無い a タグのテストをしようとしてハマったのでメモ

ボタン / リンクの存在

ボタン

buttonsubmit

<button>ボタンのラベル</button>
<input type="submit" value="ボタンのラベル" />
expect(page).to have_button 'ボタンのラベル'
リンク

a タグはボタンではなくリンクでないとマッチしない

<a href="example">リンクテキスト</a>
expect(page).to have_link 'リンクテキスト'

リンク先 (href) も含めてマッチ

expect(page).to have_link 'リンクテキスト', href: 'example'

ボタン / リンクのクリック

Capybara でクリックさせる

ボタン

buttonsubmit

<button>ボタンのラベル</button>
<input type="submit" value="ボタンのラベル" />
click_button 'ボタンのラベル'
リンク

a タグ

<a href="example">リンクテキスト</a>
click_link 'リンクテキスト'

ボタンまたはリンク

button, submit, 'a' タグ どれでもマッチする

<button>ボタンのラベル</button>
<input type="submit" value="ボタンのラベル" />
<a href="example">リンクテキスト</a>
click_on 'ボタンのラベル'
click_on 'リンクテキスト'

href が無い a タグは have_link, click_link, click_on にマッチしない

<a>リンクテキスト</a>
expect(page).to have_link 'リンクテキスト'

👇

Failure/Error: expect(page).to have_link 'リンクテキスト'
  expected to find link "リンクテキスト" but there were no matches

href が無い a タグはマッチせず存在しないと言われてしまいました…

a tags without an href are not links, they are placeholders for links. That's how the HTML spec defines it, that's how every modern browser treats them. Capybara does indeed only click on links which have the href attribute, and imho, that's sensible behaviour. click_link 'foo' not working on links without href · Issue #379 · teamcapybara/capybara · GitHub

href がないとリンクではないからと言うことのみたい

href のない a タグの存在

have_selector を使う

<a>リンクテキスト</a>
expect(page).to have_selector('a', text: '<リンクテキスト>')

href のない a タグをクリック

find で要素を選択してクリックさせる

<a>リンクテキスト</a>
find('a', text: 'リンクテキスト').click

a タグに CSS のクラス名があるならクラス名で find してもOK

<a class="link_icon">
  <i class="icon"></i>
</a>
find('.link_icon').click

 

RSpec 時々しか書かないから何も分からん…


[参考]