JavaFX + JUnit で JavaScript のユニットテストをする その 2

昨日のエントリー は、思っていたよりも反響を呼んでしまってビックリしている櫻庭です。

今日はちょっとだけ追加。

というのも、Jenkins の川口さん ( id:kkawa ) から次のようなことを聞かれたからです。


<p id="addtext"></p>

そして、HTML でボタンなどをクリックした時にコールされる関数が次のようなものだったとします。

function addText(arg) {
    var element = document.getElementById('addtext');
    element.innerText = arg;
}

タグにテキストを追加するという関数です。

これを WebEngine を使って、コールすることを考えます。基本的には昨日の手法と同じです。

では、DummyApplication クラスに addTextTest メソッドを追加します。

    public Object addTextTest(Object obj) throws InterruptedException {
        arguments.put(obj);        

        Platform.runLater(new Runnable() {
            @Override
            public void run() {
                try {
                    Object obj = arguments.take();
                    engine.executeScript("addText('"+obj+"');");
                    
                    Document document = engine.getDocument();
                    Element element = document.getElementById("addtext");
                    results.put(element.getTextContent());
                } catch (InterruptedException ex) {}
            }
        });
            
        return results.take();
    }

昨日は WebEngine クラスの executeScript メソッドの戻り値をキューに入れていたのですが、今日はそうではありません。

その代わりに、DOM の要素を取得して、そのコンテンツをキューに入れています。

JUnit のテストコードでは次のように記述しました。

    @Test
    public void addTextTest() throws InterruptedException {
        System.out.println("addTextTest");
        
        String expected = "Hello, World!";
        String result = (String)dummy.addTextTest(expected);
        assertThat(result, is(expected));

        expected = "";
        result = (String)dummy.addTextTest(expected);
        assertThat(result, is(expected));
    }

このようにすれば、DOM のチェックもできるので、DOM を操作するタイプの JavaScript の関数でもテストできます。

そういえば、昨日は 1 つのテストしか実行していなかったので、 @Before アノテーションJavaFX の EDT を立ち上げて、@After アノテーションで EDT をシャットダウンするので動作していました。

でも、複数のテストを行いのであれば、@BeforeClass で EDT を立ち上げて、@AfterClass で EDT をシャットダウンしなくてはダメですね。

さて、川口さんには DOM のテストもできるとツィートしたのですが、その後再び川口さんから返信。


package test;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import javafx.application.Application;
import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.junit.Test;

import static org.junit.Assert.assertThat;
import static org.hamcrest.CoreMatchers.is;

public class JSTest {
    private static DummyApplication dummy;
    private static ExecutorService service;
    
    public JSTest() {}

    @BeforeClass
    public static void setUpClass() throws Exception {
        service = Executors.newFixedThreadPool(1);
        service.submit(new Runnable() {
            @Override
            public void run() {
                Application.launch(test.DummyApplication.class);
            }
        });
        
        Thread.sleep(1000L);
        
        dummy = DummyApplication.getInstance();
    }

    @AfterClass
    public static void tearDownClass() {
        service.shutdown();
        dummy.shutdown();
    }

    @Test
    public void numberTest() throws InterruptedException {
        System.out.println("numberTest");
        
        Integer result = (Integer)dummy.numberTest(new Integer(3));
        assertThat(result, is(new Integer(4)));

        result = (Integer)dummy.numberTest("23");
        assertThat(result, is(new Integer(24)));
    }

    @Test
    public void addTextTest() throws InterruptedException {
        System.out.println("addTextTest");
        
        String expected = "Hello, World!";
        String result = (String)dummy.addTextTest(expected);
        assertThat(result, is(expected));

        expected = "";
        result = (String)dummy.addTextTest(expected);
        assertThat(result, is(expected));
    }
}

次に DummyApplication.java です。

package test;

import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import javafx.application.Application;
import javafx.application.Platform;
import javafx.scene.web.WebEngine;
import javafx.stage.Stage;
import org.w3c.dom.Document;
import org.w3c.dom.Element;

public class DummyApplication extends Application {
    private static DummyApplication instance;
    
    private WebEngine engine;
    private BlockingQueue<Object> arguments = new LinkedBlockingQueue<>(); 
    private BlockingQueue<Object> results = new LinkedBlockingQueue<>(); 
    
    @Override
    public void start(Stage stage) throws Exception {
        instance = this;

        engine = new WebEngine();
        engine.load( [index.htmlのURL] );
    }
    
    public static DummyApplication getInstance() {
        return instance;
    }
    
    public void shutdown() {
        Platform.runLater(new Runnable() {
            @Override
            public void run() {
                Platform.exit();
            }
        });
    }

    public Object numberTest(Object obj) throws InterruptedException {
        arguments.put(obj);        

        Platform.runLater(new Runnable() {
            @Override
            public void run() {
                try {
                    Object obj = arguments.take();
                    Object result = (Integer)engine.executeScript("numberTest("+obj+");");
                    results.put(result);
                } catch (InterruptedException ex) {}
            }
        });
            
        return results.take();
    }

    public Object addTextTest(Object obj) throws InterruptedException {
        arguments.put(obj);        

        Platform.runLater(new Runnable() {
            @Override
            public void run() {
                try {
                    Object obj = arguments.take();
                    engine.executeScript("addText('"+obj+"');");
                    
                    Document document = engine.getDocument();
                    Element element = document.getElementById("addtext");
                    results.put(element.getTextContent());
                } catch (InterruptedException ex) {}
            }
        });
            
        return results.take();
    }
}