JavaFXのWebViewでSeleniumみたいなWebテストやってみたら面白かったんだけど

JavaFX2.0のWebViewで、Javaからブラウザが操作できるようになって、これをSeleniumが組み込んだらおもしろいことになるだろうなと思ってたのですが、よくよく考えると、SeleniumナシでWebサイトのUIテストが簡単に書けることに気づきました。


ということで、まずはテスト対象のサイトをつくってみます。
最初の画面(first.html)。テキストフィールドがあって、OKボタンを押すと「ほんと?」という確認ダイアログがでて、「OK」を押すと次の画面に行きます。

<!DOCTYPE html>
<html>
    <head>
        <title>テスト画面</title>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
    </head>
    <body>
        <h1>テスト画面</h1>
        <form id="myform" action="next.jsp" method="post"
              onsubmit="return confirm('ほんと?')">
            <input type="text" name="param" id="param">
            <input type="submit" value="OK">
        </form>
    </body>
</html>


で、その遷移先の画面(next.jsp)。入力された文字を表示します。

<%@page contentType="text/html" pageEncoding="UTF-8"%>
<% request.setCharacterEncoding("utf-8"); %>
<!DOCTYPE html>
<html>
    <head>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
        <title>結果よ</title>
    </head>
    <body>
        <h1>結果よ</h1>
        <div id="result">${param['param']}でした</div>
    </body>
</html>


さて、これをテストするプログラムを書いてみます。ソースは最後に貼っておきます。
実行すると、first.htmlが表示されて、自動的にテキストフィールドに文字が入力されます。


で、画面が遷移します。


このとき、遷移先URL、タイトル、メッセージ文字列を確認します。

フォーム取得:成功
遷移:成功
タイトル:成功
結果:成功


Seleniumっぽいことができました!


Seleniumってブラウザ表示させるので、起動が重かったり結構時間がかかるのですけど、かなり軽くWebテストができます。
あと、プログラム工夫すれば、通らなかったテストケースを画面キャプチャして確認できるようにしたり、便利な感じにできます。
画面を2つ表示して別々のユーザーでログインさせて、互いの操作が反映するか確認するっていうテストはSeleniumではできなかったのですが、それもできそう。
しかもこれ、WebViewで画面に表示する必要はなくて、WebEngineだけ準備すればいいです。Seleniumのテストは画面が表示されて、結構うっとおしかったので、定期自動テストでもだいじょうぶ。


ただ、本来は確認ダイアログが出るはずなのだけど、それは出ませんでした。
WebEngineにsetConfirmHandlerとかそれっぽいメソッドがあるのですけど、呼び出されてくれません。
あと、なぜか何回かに1度JavaScriptのパースエラーが出たり、処理終了後に例外メッセージが出てたり、いまの段階では動作が安定しているとはいえません。まあ、ベータなので本番リリースまでに直ってくれればいいのですけど。


というわけでソースです。
いつもどおり、コンパイルや実行にはJavaFX2.0ベータのjfxrt.jarをクラスパスに含めます。64bit Windowsでも32bit JDKで動かす必要があります。

package javafxapplication;

import java.awt.BorderLayout;
import javafx.application.Application;
import javafx.application.Platform;
import javafx.async.Task;
import javafx.async.TaskEvent;
import javafx.embed.swing.JFXPanel;
import javafx.event.EventHandler;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.web.WebEngine;
import javafx.scene.web.WebView;
import javafx.stage.Stage;
import org.w3c.dom.Document;

public class WebTestFrame extends javax.swing.JFrame {
    WebEngine engine;
    JFXPanel panel;

    public WebTestFrame() {
        initComponents();

        TmpFxLauncher.launch();

        panel = new JFXPanel();
        this.add(panel, BorderLayout.CENTER);

        Platform.runLater(new Runnable() {
            @Override
            public void run() {
                final String ROOT = "http://localhost:8080/JafaFXTestWebApplication";
                //engine = new WebEngine(ROOT + "/first.html");

                //(2013/4/14 現時点のJavaSE7(update9)添付JavaFXで動くように修正)
                //WebView view = new WebView(engine);
                WebView view = new WebView();
                engine = view.getEngine();
                engine.load(ROOT + "/first.html");

                Group root = new Group();
                root.getChildren().add(view);
                Scene scene = new Scene(root);
                panel.setScene(scene);
                
                Task<Void> task = engine.getLoadTask();
                task.setOnDone(new EventHandler<TaskEvent>() {
                    @Override
                    public void handle(TaskEvent event) {
                        new Thread(new Runnable() {
                            @Override
                            public void run() {
                                Document doc = engine.getDocument();
                                sleep(interval);
                                doc.getElementById("param").setAttribute("value", "おうおう");
                                sleep(interval);
                                Object o = doc.getElementById("myform");
                                assertMessage("フォーム取得", o != null);
                                engine.executeScript("document.getElementById('myform').submit()");
                                sleep(interval);
                                String location = engine.getLocation();
                                assertMessage("遷移", (ROOT + "/next.jsp").equals(location));
                                String title = engine.getTitle();
                                assertMessage("タイトル", "結果よ".equals(title));
                                doc = engine.getDocument();
                                String result = doc.getElementById("result").getTextContent();
                                assertMessage("結果", "おうおうでした".equals(result));
                                

                            }
                        }).start();
                    }
                });
                
            }
        });
    }
    /** コマンド発行間隔 */
    int interval = 200;

    /** スリープ */
    static void sleep(long t){
        try {
            Thread.sleep(t);
        } catch (InterruptedException ex) {
        }
    }

    /** メッセージ表示 */
    static void assertMessage(String msg, boolean result){
        System.out.printf("%s:%s%n", msg, (result ? "成功" : "失敗"));
    }

    /** 仮アプリケーション */
    public static class TmpFxLauncher extends Application {
        @Override public void start(Stage primaryStage) {
        }

        private static void launch() {
            Application.launch(null);
        }
    }    
    
    @SuppressWarnings("unchecked")
    private void initComponents() {

        setDefaultCloseOperation(javax.swing.WindowConstants.EXIT_ON_CLOSE);
        setTitle("WebView Browser");

        java.awt.Dimension screenSize = java.awt.Toolkit.getDefaultToolkit().getScreenSize();
        setBounds((screenSize.width-616)/2, (screenSize.height-488)/2, 616, 488);
    }

    public static void main(String args[]) {
        java.awt.EventQueue.invokeLater(new Runnable() {

            public void run() {
                new WebTestFrame().setVisible(true);
            }
        });
    }
}