reactive-web: Lift をシンプルに

テスト

Reactive-web は、ユーザーインタフェースのテストを非常に強力にサポートしています。具体的には、Selenium が通常使われるようなほとんどのテストを実行することができ、しかもブラウザや Jetty を実行する必要はありません。また、テストではスニペットクラスなどのサーバーオブジェクトにもダイレクトにアクセスできます。簡単な例を示します。

  val ts: TestScope = ...
  def phone = ts / "#phone"  // id 'phone' を持つ要素
  // 入力します (これは text input です).
  phone sendKeys cust.home_phone.is
  // 入力した値をスニペットが受け取ったことをチェックします.
  snippet.phone.now should equal (cust.home_phone.is)
  // DOM が更新されたことをチェックします (入力した電話番号の顧客が読み込まれます).
  ts / ".firstname" attr "value" should equal (cust.firstName.is)
  

このテストは次の 2 つのことを利用して作られています。すなわち、(1) reactive でのすべての更新は、スレッドローカルな現在の Scope で行われること、(2) DOM の更新は、セマンティックな (case クラス) 表現を持っていること、この 2 つです。動作のしくみですが、TestScope という Scope があって、これが (ブラウザの理論上のコンテンツまたはその一部を表す) XML を保持しており、DOM の更新が呼び出されるたびに、自分が保持している XML に変換を適用するようになっています。そのため、ブラウザの DOM がどのようになっているかを常に把握することができます。また、イベントをシミュレートすることができ、DOM を非常に簡潔にチェックするための DSL を持っています。

セットアップ

通常、テストは net.liftweb.mockweb.MockWeb.testS("/") { ... } で囲んで、実際の HTTP コンテキストの外部でスニペットを実行できるようします。

次に、ブラウザの初期状態を生成する必要があります。そのため、作成したスニペットクラスをインスタンス化して、これらのスニペットを使ってテンプレートを実行するよう Lift に指示する必要があります。次に例を示します。

  val snippet = new EditOrder
  val page = {
    S.mapSnippet("EditOrder", snippet.render)
    S.runTemplate("orders" :: "edit" :: Nil).open_!
  }
  

(上の例では open_! を使用していますが、テンプレートを実行できなかった場合はテストをアボートしたいので問題ありません。)

次に、出力を指定して TestScope をインスタンス化し、これをテスト用のスコープとして使用します。また、このスコープのメンバもインポートします。いくつか重要な暗黙の変換が用意されているからです。

  val ts = new TestScope(page)
  import ts._
  Reactions.inScope(ts) {
    // ここにテストを記述
  }
  

DOM へのアクセスと操作

ts.xml と記述することで、NodeSeq として現在の xml にアクセスできます。xml の分析には、\\\ によって利用可能なシンプルな XPath サポートや、コレクションの find など、Scala の既存の機能のすべてを使用できます。さらに、TestScope は (暗黙の変換を使って) 強力な検索ツールをいくつか提供しています。検索演算子は 6 つあって、node / pred または node / (pred1, ... predN) のようにして使用し、すべての述語を満たす 1 つまたは複数のノードが返されます。述語については、あとで説明します。6 つの演算子はすべて varargs を取るので、上のどちらの形式でも使用できます。また、これらの演算子は、Seq[Node] (すべての Node がそうです) に対しても、TestScope それ自体に対しても使用できます。

上で述べた 6 つの演算子とは、 //?/+>>?、および >+ です。/ は、すべての子孫 (および該当ノードそれ自体) を検索するのに対し、> は直接の子しか検索しません。?Option[Node] を返すことを意味し、+Seq[Node] を返すことを意味します。サフィックスがない場合は Node を返すか、Node が見つからなかった場合は例外をスローします。

述語は任意の Node=>Boolean で、一般的な述語を文字列として記述できる便利な暗黙の変換です。文字列は、"label""#id"".class"":name""=テキストとの完全一致"、または "~テキストの部分文字列" の形式で記述できます。例を示すと、XPath 表現 //fieldset[legend/text()='Items'] (Selenium ではこのようになります) の代わりに、ts / ("fieldset", _ /?("legend", "=Items") isDefined) と記述することができます。この記述は、ラベルが "fieldset" (最初の述語) で、2 番目の述語として渡された関数が真を返すノードを探すことを意味しています。この関数は、ラベルが "legend" でテキストの内容が "Items" であるノードを fieldset から探します。この例では /? を使っているので、もし結果があれば、その結果は Option に入れられ、この Option に対して isDefined を呼び出すことで、ノードが存在するかどうかを Boolean で示すことができ、有効な述語関数になっています。

なお、省略記法として、testScope("anId") と書くだけで id でノードを取得できます。

NodePowerNode としてラップする暗黙の変換も用意されています。この PowerNode には、次に示すようないくつかの便利なメンバがあります。

attr
Map[String, String] としての属性
update
このノードの属性値を更新したもので TestScope の DOM を置き換えます。ノードそれ自体は immutable で、新しい値を持つわけではありませんが、update は新しいノードを参照して、そのノードを返します。ただしそれには、ノードが id 属性を持っている必要があります (また、id 属性を変更しないことが前提になります)。使用法: node("attribute") = "value"
id
id 属性の値
clazz
class 属性の値
classes
ホワイトスペースで区切られた class 属性からなる Set[String]
value
value 属性の値。値を設定することもでき (node.value = "xyz")、その場合は update が呼び出されます。
fire(event)
このノードで発火されるイベントをシミュレートします。eventDomEvent です。ノードの対応するイベント属性を分析して、発火されるイベントとプロパティの更新がないかどうかを調べることで動作します。現在実装されているイベントは、Change、Click、および KeyUp だけです。
sendKeys(text)
フィールドへの入力をシミュレートします。text には、バックスペースとして扱われる '\b' を含めることができます。1 文字ごとに value 属性を更新し、KeyUp イベントを発火し、最後に Change イベントを発火します。

Selenium と比較した場合の制限

  1. XPath は使用しません。"//fieldset[legend/text()='Items']" の代わりに、ts / ("fieldset", _ /?"=Items" isDefined) のように記述してください。
  2. 現在、alert、confirm などはサポートされていません。
  3. 現在、reactive-web によって生成された javascript でのみ動作します。
  4. イベントは人工的にシミュレートされるので、ブラウザのイベント処理方法が基になる実際の動作とは、異なる部分がある可能性があります。
  5. 実際に起きていることを目で確認することはできません。もちろん、ステップごとに xml をどこかにレンダリングすれば、この問題は回避できます。1 つの方法は、Flying Saucer のようなものを使って、Swing コンポーネントに xml をレンダリングすることです。また、Web アプリケーション内からテストを実行し、xml を DOM に挿入して表示させる方法もあります。