目次
1. TemplateMethod パターン
Java 100 本ノックを使わせていただき、Java の実装力を上げるべく取り組んでいます。73 本目では、クラス図を見て実装をする問題で、TemplateMethod パターンを使用していました。これまで抽象クラスを実装したことがなくいい練習になったのですが、テストをどう書くか悩みました。中朝クラスを継承するクラスのテストでまとめてテストすることも可能ですが、今回実装クラスが 2 つあるので同じようなテストを 2 回書くのも気が引けるし、何より保守性に問題を感じます。
抽象クラスをテストする方法として、テストで匿名クラスを実装するという方法があるようですが、具体的なテストに実装を書くのはしっくりこない、かつテストケース毎に抽象メソッドの挙動を変えたいとなると、コードの記述量も多くなりそうです。
そんな課題は Mockito で解決できそう。Spy を使い抽象クラスをインスタンス化することで、実装しているメソッドはそのまま使えます。さらに抽象メソッドの挙動をテストケース毎に簡単に変更可能です。
この記事では抽象クラスのテストに焦点をあてるので、実装の全体が気になる方は、後述する GitHub を見ていただけたらなと思います。
2. 環境情報
- Java 17
- JUnit 5: 5.10.1
- Mockito: 5.7.0
- 今回扱うコードはこちらの GitHub リポジトリ に
3. 実装
package org.contourgara;
import lombok.Getter;
@Getter
public abstract class AbstractCommand<T> implements Command<T> {
private Status status = Status.NONE;
private T result = null;
private Exception exception = null;
protected abstract T executeInner();
@Override
public void execute() {
try {
status = Status.EXECUTING;
result = executeInner();
status = Status.SUCCESS;
} catch (Exception e) {
status = Status.ERROR;
result = null;
exception = e;
}
}
}
この抽象クラスの責務は、executeInner()
メソッドの実行による状態の変更と、実行結果や発生した例外の変数へのセットなので、それだけをテストするとこのクラスの実装を明確にできるテストが書けると考えました。そうすることで、このクラスを継承するクラスのテストもシンプルになりそうです。
4. テスト
package org.contourgara;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.MockitoAnnotations;
import org.mockito.Spy;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.doThrow;
class AbstractCommandTest {
private AutoCloseable closeable;
@Spy
AbstractCommand<?> abstractCommand;
@BeforeEach
void setUp() {
closeable = MockitoAnnotations.openMocks(this);
}
@AfterEach
void tearDown() throws Exception {
closeable.close();
}
@Test
void コマンド実行前() {
// assert
assertThat(abstractCommand.getStatus()).isEqualTo(Status.NONE);
assertThat(abstractCommand.getResult()).isNull();
assertThat(abstractCommand.getException()).isNull();
}
@Test
void コマンド終了後() {
// setup
doReturn(1).when(abstractCommand).executeInner();
// execute
abstractCommand.execute();
// assert
assertThat(abstractCommand.getStatus()).isEqualTo(Status.SUCCESS);
assertThat(abstractCommand.getResult()).isEqualTo(1);
assertThat(abstractCommand.getException()).isNull();
}
@Test
void 例外発生時() {
// setup
doThrow(new RuntimeException()).when(abstractCommand).executeInner();
// execute
abstractCommand.execute();
// assert
assertThat(abstractCommand.getStatus()).isEqualTo(Status.ERROR);
assertThat(abstractCommand.getResult()).isNull();
assertThat(abstractCommand.getException()).isInstanceOf(RuntimeException.class);
}
}
Spy はアノテーションで定義しました。色々調べてもアノテーションで定義している例はなかなか出てきませんが、シンプルに書けていいと思います。注意点としては、初期化をしてあげる必要があることです。今回はテストケース毎にメソッドの挙動を変更するため、BeforeEach に初期化を書きました。
executeInner()
の挙動をテストメソッド毎に変更しています。コマンド終了後では、1 を返すメソッド、例外発生時では、RuntimeException を Throw するようにすることで、この抽象クラスがやることを網羅的にテストしています。executeInner()
実行直前の状態のテストはできていませんが......
抽象クラスで具体的な実装に依存しないテストが書けました。
5. まとめ
テストクラスでやっていることが明確だと、実装も理解しやすくて気持ちが良いですね。
レガシーコードとは、単にテストのないコードである
(cf. レガシーコード改善ガイド)