2012年3月9日金曜日

JavaFX + JUnit で javascriptのunit testできるようにしてやるんで、これからハマっていってやんよ - 2

どーも、季節性鬱症がかなりひどくてやる気が0なミケです。


ツイッターでJavaFXの同期がむずいとつぶやいていたら、@skrbさんから



こんなツイートをいただきました。

JavaFXとJUnitのThreadについて丁寧に解説されていて、動作できたようです。

というわけで、コピペプログラマーとしてはコピペしないわけにはいきません。

早速Jettyも使ってunit testできるようにしましょう。


@BeforeClassで起動するWebサービス


JUnitのテストコードの中でServerを持っているのが大分辛くなってきたので、

Serverのコードは外に出すことにしました。

まだ、試作段階なので、Handlerクラスなども固定でしか動きません。

package org.mikeneck.jfx.server;
import org.eclipse.jetty.server.Server;
import org.mikeneck.jfx.handler.RequestHandler;
import java.net.InetSocketAddress;
/**
* @author: mike
* @since: 12/03/07
*/
public class MockWebServer {
private Server server;
private String hostName;
private int port;
public MockWebServer() {
createServer("localhost", 3080);
}
public MockWebServer(String hostName, int port) {
createServer(hostName, port);
}
private void createServer(String hostName, int port) {
this.hostName = hostName;
this.port = port;
InetSocketAddress address = new InetSocketAddress(hostName, port);
server = new Server(address);
setUpServer();
}
private void setUpServer () {
RequestHandler handler = new RequestHandler(server);
server.setHandler(handler);
}
public String getHostName() {
return hostName;
}
public int getPort() {
return port;
}
public String getUrl () {
StringBuilder builder = new StringBuilder();
String url = builder.append("http://")
.append(getHostName())
.append(":")
.append(getPort())
.toString();
return url;
}
public void start() throws Exception {
server.start();
}
public void stop() throws Exception {
server.stop();
server.destroy();
}
}



一応、サーバーのアドレスとかポートも設定できるようにしておきたいので、

コンストラクターで指定できるようにはしてあります。

ただし、現段階では使っていません。


TestBrowser


JavaFXのWebEngineを使ってテストコードを走らせるクラスです。

@skrbさんのブログの内容をそのままコピーしています。

なお、一部分を変更しています。

どうやら@Testメソッドを実行時に、画面のロードが完了していないなどの同期の部分で

失敗したため、javascript上でロードしたことをマークするようにして、ロードが完了するまでは待機するように

改造してあります。

package org.mikeneck.jfx;
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;
import java.util.List;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
/**
* @author: mike
* @since: 12/03/04
*/
public class TestBrowser extends Application {
private static TestBrowser instance;
private static boolean ready = false;
private WebEngine webEngine;
private BlockingQueue<String> script;
@Override
public void start(Stage stage) throws Exception {
instance = this;
String url = getUrl();
webEngine = new WebEngine();
webEngine.load(url);
ready = true;
}
private String getUrl() throws IllegalStateException {
Parameters parameters = getParameters();
List<String> params = parameters.getRaw();
if (params == null || params.size() == 0) {
throw new IllegalStateException("Url is not defined.");
}
return params.get(0);
}
public static TestBrowser getInstance() throws IllegalStateException {
if (instance == null) {
throw new IllegalStateException("Java FX Application is not launched.");
}
return instance;
}
public static boolean isReady() {
if (instance == null) {
return false;
}
return ready;
}
public void shutdown() {
Platform.runLater(
new Runnable() {
@Override
public void run() {
Platform.exit();
ready = false;
instance = null;
}
}
);
}
public <T> T runScript(String source, final Class<T> expectedClass) throws InterruptedException, ClassCastException {
script = new LinkedBlockingQueue<>();
script.put(source);
final BlockingQueue<T> results = new LinkedBlockingQueue<>();
Platform.runLater(
new Runnable() {
@Override
public void run() {
try {
/** for synchronization of loading contents **/
Document document = webEngine.getDocument();
Element element = document.getElementById("loaded");
String content = element.getTextContent();
while ("".equals(content)) {
Thread.sleep(100);
element = document.getElementById("loaded");
content = element.getTextContent();
}
String command = script.take();
Object result = webEngine.executeScript(command);
T castedResult = expectedClass.cast(result);
results.put(castedResult);
} catch (InterruptedException e) {
e.printStackTrace();
try {
results.put(null);
} catch (InterruptedException e1) {
}
}
}
}
);
return results.take();
}
}



なかなか、ソースが読みづらくてごめんなさい。試作段階ということで許しておくれ。

Threadの同期と非同期は難しいですね。

@skrbさんには非常に感謝しております。



実際のテストコード


実際のテストコードです。

これは単純に数値を計算するfunctionを呼び出しているだけです。

package org.mikeneck.jfx;
import javafx.application.Application;
import org.junit.*;
import org.mikeneck.jfx.server.MockWebServer;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import static org.hamcrest.CoreMatchers.is;
import static org.junit.Assert.assertThat;
/**
* @author: mike
* @since: 12/03/07
*/
public class JsJUnit {
private TestBrowser testBrowser;
private ExecutorService executorService;
private static MockWebServer server;
@BeforeClass
public static void setUpWebServer() throws Exception {
server = new MockWebServer();
server.start();
}
@AfterClass
public static void tearDownServer () throws Exception {
server.stop();
}
@Test
public void doNumberTest() throws InterruptedException {
Integer value = testBrowser.runScript("numberTest(1)", Integer.class);
assertThat(value, is(2));
}
@Test
public void doStringTest() throws InterruptedException {
String value = testBrowser.runScript("stringTest('test')", String.class);
assertThat(value, is("test_test"));
}
@Before
public void setUp () throws InterruptedException {
executorService = Executors.newFixedThreadPool(1);
executorService.submit(
new Runnable() {
@Override
public void run() {
Application.launch(TestBrowser.class, server.getUrl());
}
}
);
while (TestBrowser.isReady() == false) {
Thread.sleep(100);
}
testBrowser = TestBrowser.getInstance();
}
@After
public void tearDown () {
executorService.shutdown();
testBrowser.shutdown();
}
}
view raw JsJUnit.java hosted with ❤ by GitHub



なお、@BeforeにてApplication#launch(Class<T extends Application>, java.lang.String[])

第二引数でサーバーのURLを渡してあります。

これは、アプリケーション側でgetParameter()メソッドを使うと取得することができます。

サーバーのポートなどの問題でURLを変更することはよくあるので、

この辺は可変にしておきたかったです。



実行結果


実行結果は次のような感じになります。



お気づきな方もいると思いますが、doStringTestの方は@Ignoreしています。

これ、@Ignoreを外すと、2つ目のテストが今は動かない状態なんです。



これはTestBrowserの部分の作りがまだまだ粗いため、2つ目のテストが実行できない状態になっているからですね。

TestBrowserの起動を@BeforeClassに移動して、

@Before毎にページを読み直すような仕様に変更しようと考えています。


とりあえず、今はこんな感じです。


どうやら…


@skrbさんの周囲で、

JRubyのなひさんJenkinsの川口さんといった

錚々たる面々のお方から反応があったらしく、

意外と面白そうなプロダクトになりそうな気がしてきました。


Seleniumと比べて


SeleniumなどのUIテスト系と比べて、僕がこのFxJsJUnitでやりたかったのはjavascriptのunit testなので、

実はUIはあまり気にしていないんです。

なので、縦幅がいくつとかはあまり興味がなくて、

純粋にjavascriptの関数に対してTDDを実施していけるような状態にしたいというのが第一の目標です。

あと@skrbさんのブログにあったとおり、

GUIは別に表示しなくてもいいという点からいくと、

Seleniumのように画面がポコポコ生まれなくていいというのが強みかなと思っていたりします。

あとはwebkit搭載なので、巷に最近溢れているブラウザー(Chrome/Android用のブラウザ/Safari)に対応できるので、

モバイル系のアプリにも対応できるテストツールになりうるかもしれません。


いずれにせよ、もう少し安定して動かせるようにしたいところです。


なお、コードはgist( https://gist.github.com/2001562 )上にあげていますので、ぜひご意見等いただければ幸いです。

あと、そのうちちゃんとしたプロジェクトとしてgithubにプロジェクトを作りたいと思っています。







0 件のコメント:

コメントを投稿