目次
・本日の成果・考え
・最後に
本日の成果
本日はテストパターンの想定およびテストソースのPG・テスト実行までやっていこうと思います。
UT完了まではできなかったので、動作確認をしようと思います。
前回予想した通り、テスト実行にはモック化が必要ということでした。
以下よりJarファイルをダウンロードしました。
ダウンロードしたファイル
・mockito-core-5.12.0.jar
・byte-buddy-1.14.14.jar
・byte-buddy-agent-1.14.15.jar
とりあえず動くかどうか試してみます。
package app.test;
import static java.lang.System.*;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*;
import java.util.function.Consumer;
import org.junit.Test;
import app.windowView.api.DifyApiClient;
import app.windowView.window.WindowController;
import javafx.scene.web.WebEngine;
import javafx.scene.web.WebView;
public class WindowControllerTest {
@Test
public void test1() {
String className = new Object() {
}.getClass().getName();
String resultOutput = className + "のテストパターン1_1";
out.println("**********************************************");
out.println(resultOutput + "が開始されました。");
try {
// ウィンドウ用モック生成
WebView mockView = mock(WebView.class);
WebEngine mockEngine = mock(WebEngine.class);
when(mockView.getEngine()).thenReturn(mockEngine);
DifyApiClient mockApiClient = mock(DifyApiClient.class);
// モックのcontroller
WindowController controller = new WindowController(mockView, mockApiClient);
// APIモック設定
doAnswer(invocation -> {
Consumer<String> chunkConsumer = invocation.getArgument(1);
Runnable completeCallback = invocation.getArgument(2);
chunkConsumer.accept("テストチャンク");
completeCallback.run();
return null;
}).when(mockApiClient).streamingMsg(any(), any(), any());
// テスト実行
controller.onSendMessage("テストメッセージ");
// JavaScript呼び出しの確認(エスケープ処理済み)
verify(mockEngine).executeScript(contains("appendMsg"));
verify(mockEngine).executeScript(contains("completeMsg"));
out.println(resultOutput + "が正常終了しました。");
} catch (Exception e) {
String resultError = String.format("エラーが発生しました。内容は{%s}", e);
out.println(resultError);
}finally {
out.println(resultOutput + "が終了しました。");
out.println("**********************************************");
}
}
}
各クラスのモック化と、API処理の応答の中身をモック化しました。
エラーが発生しました。内容は{org.mockito.exceptions.base.MockitoException:
Mockito cannot mock this class: class javafx.scene.web.WebEngine.
Can not mock final classes with the following settings :
- explicit serialization (e.g. withSettings().serializable())
- extra interfaces (e.g. withSettings().extraInterfaces(...))
You are seeing this disclaimer because Mockito is configured to create inlined mocks.
You can learn about inline mocks and their limitations under item #39 of the Mockito class javadoc.
なにやら、モックとして用意した中で、JavaFXのWebEngineがfinalクラスだからモック化できなかったみたいです。
で、その解決策として、WebEngineをラッパークラスで抽象化して使うのが良いみたい。
なんだよラッパークラスって使ったことないよ。
というわけで、WebEngineからJSを呼び出す流れをラップしてみました。
package window_interface;
public interface JsCall {
void call(String js);
}
package app.windowView.window;
import javafx.scene.web.WebEngine;
import window_interface.JsCall;
public class WebEngineWrapper implements JsCall{
private final WebEngine webEngine;
public WebEngineWrapper(WebEngine webEngine) {
this.webEngine = webEngine;
}
/************
* メソッド名:JsCallの実装メソッド
* 処理内容:引数のJavaScriptを呼びだす。
* @param js 呼び出し対象のJavaScript(文字列)
/************/
@Override
public void call(String js) {
webEngine.executeScript(js);
}
public WebEngine getEngine() {
return webEngine;
}
}
合わせてテストソースの修正
@Test
public void test1() {
String className = new Object() {
}.getClass().getName();
String resultOutput = className + "のテストパターン1_1";
out.println("**********************************************");
out.println(resultOutput + "が開始されました。");
try {
// ウィンドウ用モック生成
WebEngineWrapper mockEngine = mock(WebEngineWrapper.class);
DifyApiClient mockApiClient = mock(DifyApiClient.class);
// モックのcontroller
WindowController controller = new WindowController(mockEngine, mockApiClient);
// APIモック設定
doAnswer(invocation -> {
Consumer<String> chunkConsumer = invocation.getArgument(1);
Runnable completeCallback = invocation.getArgument(2);
chunkConsumer.accept("テストチャンク");
completeCallback.run();
return null;
}).when(mockApiClient).streamingMsg(any(), any(), any());
// テスト実行
controller.onSendMessage("テストメッセージ");
// JavaScript呼び出しの確認(エスケープ処理済み)
verify(mockEngine).call(contains("appnedMsg"));
verify(mockEngine).call(contains("completeMsg"));
out.println(resultOutput + "が正常終了しました。");
} catch (Exception e) {
String resultError = String.format("エラーが発生しました。内容は{%s}", e);
out.println(resultError);
}finally {
out.println(resultOutput + "が終了しました。");
out.println("**********************************************");
}
Exception in thread "Thread-0" java.lang.IllegalStateException: Toolkit not initialized
at com.sun.javafx.application.PlatformImpl.runLater(PlatformImpl.java:436)
at com.sun.javafx.application.PlatformImpl.runLater(PlatformImpl.java:431)
at javafx.application.Platform.runLater(Platform.java:168)
at app.windowView.window.WindowController.lambda$0(WindowController.java:41)
at java.base/java.lang.Thread.run(Thread.java:1583)
コントローラのonSendMsgメソッドをモックで起動しているのですが、メソッド側でPlatform.runLater()を起動しており、この処理はJavaFX Applicationのスレッドが起動していることが前提みたいでした。
このUTでは、スレッドを起動していないので例外がでるみたいです。
というわけで、Platform.runLaterの処理の箇所をラップして、この処理が成功したという結果をモック化しようと思います。
package window_interface;
public interface UiIniExecutor {
void runLater(Runnable r) ;
}
package app.windowView.window;
import window_interface.UiIniExecutor;
public class UiIniWrapper implements UiIniExecutor {
@Override
public void runLater(Runnable r) {
javafx.application.Platform.runLater(r);
}
}
上記を追加しました。
ラッパークラスを追加しても、コントラークラスからの呼び出しはほとんど変更はありません。
あくまでも、抽象化したラッパークラスのオブジェクト経由になるだけです。
UiIniWrapperオブジェクト.runLater()
では、テストソースの修正
@Test
public void test1() {
String className = new Object() {
}.getClass().getName();
String resultOutput = className + "のテストパターン1_1";
out.println("**********************************************");
out.println(resultOutput + "が開始されました。");
// 非同期処理の完了待機用
CountDownLatch latch = new CountDownLatch(2); // appendMsg + completeMsg
try {
// ウィンドウ用モック生成
WebEngineWrapper mockEngine = mock(WebEngineWrapper.class);
DifyApiClient mockApiClient = mock(DifyApiClient.class);
//UI処理用のモック
UiIniWrapper mockUiRunnable = mock(UiIniWrapper.class);
// モックのcontroller
WindowController controller = new WindowController(mockEngine, mockApiClient,mockUiRunnable);
// UI初期化用ラッパーのモック作成(runLaterを即実行)
doAnswer(invocation -> {
Runnable runnable = invocation.getArgument(0);
runnable.run(); // 即実行
latch.countDown();
return null;
}).when(mockUiRunnable).runLater(any());
// APIモック設定
doAnswer(invocation -> {
Consumer<String> chunkConsumer = invocation.getArgument(1);
Runnable completeCallback = invocation.getArgument(2);
chunkConsumer.accept("テストチャンク");
completeCallback.run();
return null;
}).when(mockApiClient).streamingMsg(any(), any(), any());
// テスト実行
controller.onSendMessage("テストメッセージ");
// 処理完了まで待機(最大2秒)
latch.await();
// JavaScript呼び出しの確認(エスケープ処理済み)
verify(mockEngine).call(contains("appendMsg"));
verify(mockEngine).call(contains("completeMsg"));
out.println(resultOutput + "が正常終了しました。");
} catch (Exception e) {
String resultError = String.format("エラーが発生しました。内容は{%s}", e);
out.println(resultError);
}finally {
out.println(resultOutput + "が終了しました。");
out.println("**********************************************");
}
}
UIスレッドをモック化して擬似的に立ち上げています。
また、立ち上げたUIスレッドが何もしないとすぐに終了してしまうので、 controller.onSendMessage()が実行されるまで維持するようにCountDownLatchを使用しています。
これで一旦動くことは確認できました。
最後に
ここまでお付き合いありがとうございます。
とりあえずテスト用のソースを動かすことができました。
正直ラッパークラスの使用が本当に必要だったかは確信をもって正しいといえません。
しかし、今後のテスト実効性を上げるための準備と考えれば必要なのかなと思います。
この記事でUTまで書いても良かったのですが、あまり長くなってもあれなので、動作確認までで一旦記事にすることにしました。
振り返りとしては、モック化って便利だなと思いつつも、テストとして確認したい範囲を明確にしないとただモック化で何もない処理を動かしたということになりかねないので注意が必要だと思いました。