ツイッターでJavaFXの同期がむずいとつぶやいていたら、@skrbさんから
@mike_neck ブログ書いたよ!! d.hatena.ne.jp/skrb/20120307
— Yuichi Sakurabaさん (@skrb) 3月 7, 2012
こんなツイートをいただきました。
JavaFXとJUnitのThreadについて丁寧に解説されていて、動作できたようです。
というわけで、コピペプログラマーとしてはコピペしないわけにはいきません。
早速Jettyも使ってunit testできるようにしましょう。
@BeforeClass
で起動するWebサービス
JUnitのテストコードの中でServerを持っているのが大分辛くなってきたので、
Serverのコードは外に出すことにしました。
まだ、試作段階なので、Handlerクラスなども固定でしか動きません。
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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上でロードしたことをマークするようにして、ロードが完了するまでは待機するように
改造してあります。
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
を呼び出しているだけです。
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | |
} | |
} |
なお、
@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 件のコメント:
コメントを投稿