目次
・本日の成果・考え
・最後に
本日の成果
本日こそ、コントローラークラスのUTをやりたいと思います。
では、早速テストパターンからです。
その前に今回テストする対象のソースを再度貼っておきます
対象ソース
package app.windowView.window;
import app.util.CommonFunction;
import app.windowView.api.DifyApiClient;
import app.windowView.api.DifyRequestDto;
public class WindowController {
//表示ウィンドウのWindowEngineのラッパーオブジェクト
private final WebEngineWrapper webEngine;
//APIt通信用のインスタンス APIのURLやキー情報を内包している。
private final DifyApiClient apiClient;
//UI処理スレッドのラッパーオブジェクト
private final UiIniWrapper uiRunnable;
/************
* メソッド名:引数付きコンストラクタ
* 処理内容:ロジッククラスで初期化する際に、現在表示中のウィンドウのオブジェクトを取得する。
* @param view ロジッククラスのフィールドにあるWindowViewオブジェクト
/************/
public WindowController(WebEngineWrapper webEngine,DifyApiClient apiClient,UiIniWrapper r) {
this.webEngine = webEngine;
this.apiClient = apiClient;
this.uiRunnable = r;
}
/************
* メソッド名:イベントハンドラーメソッド
* 処理内容:JSのイベント発火をロジッククラス経由で受け取る処理。API通信処理を呼び出す。
* @param msg ユーザー入力メッセージ
/************/
public void onSendMessage(String msg) {
//リクエストDto生成
DifyRequestDto dto = new DifyRequestDto(msg);
//API通信用のスレッド作成。通信終了後に破棄。
Thread communicationThread = new Thread(() -> {
try {
apiClient.streamingMsg(
dto,
chunk -> uiRunnable.runLater(() -> appendChatChunk(chunk)),
() -> uiRunnable.runLater(this::onChatComplete)
);
} catch (Exception e) {
uiRunnable.runLater(() -> showError(e));
}
});
communicationThread.setDaemon(true); // アプリ終了と同時に停止するよう設定
communicationThread.start(); // 実行。
}
/************
* メソッド名:エラーメッセージ表示
* 処理内容:API通信またはUI更新処理内でエラーが発生したらエラーメッセージを表示する処理を呼び出す。
* @param e
/************/
private void showError(Exception e) {
String msg = "エラーが発生しました: " + e.getMessage();
webEngine.call("showError('" + CommonFunction.escapeForJS(msg) + "')");
}
/************
* メソッド名:受信チャンク表示
* 処理内容:受信したチャンク毎に表示処理を呼び出す
* @param chunk Difyからのレスポンス
/************/
private void appendChatChunk(String chunk) {
webEngine.call("appendMsg("+ CommonFunction.escapeForJS(chunk)+")");
}
/************
* メソッド名:受信完了メッセージ表示
* 処理内容:全てのチャンクの受信が完了した旨を表示する処理を呼び出す。
/************/
private void onChatComplete() {
webEngine.call("completeMsg()");
}
}
テストパターン
- 正常系:onSendMessage処理から正しくappendChatChunkおよびonChatCompleteが呼び出される。
- 正常系:複数チャンク受信時でもパターン1と同様の結果が得られる。
- 異常系:onSendMessage実行中に例外が発生した際に、showErrorが呼び出される。
正常系と異常系と分けていますが、両方ともメッセージ処理が呼び出されるかを確認するだけです。
このテストパターンにした理由としては、テスト対象であるコントローラークラスが処理を呼び出すだけの責任しかないためです。
入力値や受け取り値は正しいことが前提なため、このクラスでは異常な値についてのテストは省くことにしました。
テストソース
package app.test;
import static java.lang.System.*;
import static org.junit.Assert.*;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
import org.junit.Test;
import app.windowView.api.DifyApiClient;
import app.windowView.window.UiIniWrapper;
import app.windowView.window.WebEngineWrapper;
import app.windowView.window.WindowController;
public class WindowControllerTest {
// ウィンドウ用モック生成
WebEngineWrapper mockEngine = mock(WebEngineWrapper.class);
//APIクラスのモック
DifyApiClient mockApiClient = mock(DifyApiClient.class);
//UI処理用のモック
UiIniWrapper mockUiRunnable = mock(UiIniWrapper.class);
// モックのcontroller
WindowController controller = new WindowController(mockEngine, mockApiClient, mockUiRunnable);
/**
*テストパターン 正常系 onSendMessage処理から正しくappendChatChunkおよびonChatCompleteが呼び出される。
*/
@Test
public void test1() {
String className = new Object() {
}.getClass().getName();
String resultOutput = className + "のテストパターン1";
out.println("**********************************************");
out.println(resultOutput + "が開始されました。");
// 非同期処理の完了待機用
CountDownLatch latch = new CountDownLatch(2); // appendMsg + completeMsg
try {
// 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秒)
boolean completed = latch.await(2, TimeUnit.SECONDS);
assertTrue("非同期処理が2秒以内に完了しませんでした", completed);
// JavaScript呼び出しの確認()
verify(mockEngine).call(eq("appendMsg(テストチャンク)"));
verify(mockEngine).call(eq("completeMsg()"));
out.println(resultOutput + "が正常終了しました。");
} catch (Exception e) {
String resultError = String.format("エラーが発生しました。内容は{%s}", e);
out.println(resultError);
} finally {
out.println(resultOutput + "が終了しました。");
out.println("**********************************************");
}
}
/**
*テストパターン 正常系 複数チャンク受信時でもパターン1と同様の結果が得られる。
*/
@Test
public void test2() {
String className = new Object() {
}.getClass().getName();
String resultOutput = className + "のテストパターン2";
out.println("**********************************************");
out.println(resultOutput + "が開始されました。");
// 非同期処理の完了待機用
CountDownLatch latch = new CountDownLatch(3); // appendMsg×2 + completeMsg
try {
// 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("テストチャンク1");
chunkConsumer.accept("テストチャンク2");
completeCallback.run();
return null;
}).when(mockApiClient).streamingMsg(any(), any(), any());
// テスト実行
controller.onSendMessage("テストメッセージ");
// 処理完了まで待機(最大2秒)
boolean completed = latch.await(2, TimeUnit.SECONDS);
assertTrue("非同期処理が2秒以内に完了しませんでした", completed);
// JavaScript呼び出しの確認()
verify(mockEngine).call(eq("appendMsg(テストチャンク1)"));
verify(mockEngine).call(eq("appendMsg(テストチャンク2)"));
verify(mockEngine).call(eq("completeMsg()"));
out.println(resultOutput + "が正常終了しました。");
} catch (Exception e) {
String resultError = String.format("エラーが発生しました。内容は{%s}", e);
out.println(resultError);
} finally {
out.println(resultOutput + "が終了しました。");
out.println("**********************************************");
}
}
/**
*テストパターン 正常系 複数チャンク受信時でもパターン1と同様の結果が得られる。
*/
@Test
public void test3() {
String className = new Object() {
}.getClass().getName();
String resultOutput = className + "のテストパターン3";
out.println("**********************************************");
out.println(resultOutput + "が開始されました。");
// 非同期処理の完了待機用
CountDownLatch latch = new CountDownLatch(1); // showError
try {
// UI初期化用ラッパーのモック作成(runLaterを即実行)
doAnswer(invocation -> {
Runnable runnable = invocation.getArgument(0);
runnable.run(); // 即実行
latch.countDown();
return null;
}).when(mockUiRunnable).runLater(any());
// APIモック設定
doThrow(new RuntimeException("API通信の失敗")).when(mockApiClient).streamingMsg(any(), any(), any());
// テスト実行
controller.onSendMessage("テストメッセージ");
// 処理完了まで待機(最大2秒)
boolean completed = latch.await(2, TimeUnit.SECONDS);
assertTrue("非同期処理が2秒以内に完了しませんでした", completed);
// JavaScript呼び出しの確認()
verify(mockEngine).call(eq("showError(エラーが発生しました: API通信の失敗)"));
out.println(resultOutput + "が正常終了しました。");
} catch (Exception e) {
String resultError = String.format("エラーが発生しました。内容は{%s}", e);
out.println(resultError);
} finally {
out.println(resultOutput + "が終了しました。");
out.println("**********************************************");
}
}
}
では、テスト実行
カバレッジ100%で、テスト結果も問題なさそうです。
一応JS起動チェックの箇所を以下のようにを変更すると
verify(mockEngine).call(eq("appendMsg(間違いチャンク)"));
Argument(s) are different! Wanted:
webEngineWrapper.call("appendMsg(間違いチャンク)");
-> at app.windowView.window.WebEngineWrapper.call(WebEngineWrapper.java:20)
Actual invocations have different arguments:
webEngineWrapper.call("appendMsg(テストチャンク)");
-> at app.windowView.window.WindowController.appendChatChunk(WindowController.java:64)
こんな感じでエラーが発生するので、正しくJSを呼び出せているみたいです。
最後に
ここまでお付き合いありがとうございます。
前々回から前回の投稿間隔がおよそ4日、前回から今回の投稿感覚が1時間。
タスクの分割の仕方が悪いのか、想定通りに作業が進まないのか。
おそらく両方だと思いますが、投稿間隔をなるべくあけないようにしたいです。
今回があまり間隔が空かなかった理由は、パターンを考えて実行するだけだったからです。
※前回で動作確認ができるソース作成済み、加えてテストパターンについても前回の投稿時点ですでに検討し終えていました。
今回の振り返りとしては、確認したい内容だけに限定したテストが今後どのようなバグを抱えているのかが怖いところですね。とはいえ、考えた限り入力・出力内容はテスト不要ですし、これで良いと思っているんですよね。
次回は、JSとJavaを繋ぐブリッジクラスのPGに入りたいと思います。