単体テストを意識した設計(DI, インターフェース分離) — テスト容易性
単体テストをしやすくするための設計の基本が 依存注入(DI: Dependency Injection) と インターフェース分離 です。
「テスト容易性」を意識すると、コードが自然に疎結合になり、保守性も高まります。初心者向けに、コード例とテンプレートを交えて解説します。
依存注入(DI)の考え方
- 依存を外から渡す: クラスが必要とするオブジェクトを「自分で new」せず、外部から渡してもらう。
- テスト容易性: 本番用の依存とテスト用のモックを切り替えられる。
- 種類: コンストラクタ注入、セッター注入、フィールド注入。基本は「コンストラクタ注入」が推奨。
例題: コンストラクタ注入
interface MailSender {
void send(String to, String body);
}
class SmtpMailSender implements MailSender {
public void send(String to, String body) {
System.out.println("SMTP送信: " + to + " " + body);
}
}
class UserService {
private final MailSender mailSender;
// 依存をコンストラクタで注入
public UserService(MailSender mailSender) {
this.mailSender = mailSender;
}
public void register(String user) {
// 登録処理...
mailSender.send(user, "ようこそ!");
}
}
Java👉 テスト時はモックを渡せる:
class DummyMailSender implements MailSender {
public void send(String to, String body) {
System.out.println("テスト用送信: " + to + " " + body);
}
}
UserService service = new UserService(new DummyMailSender());
service.register("test@example.com"); // 本番のSMTPに依存しない
Javaインターフェース分離の考え方
- 役割ごとにインターフェースを分ける: 大きなインターフェースにまとめすぎると、テストや実装が複雑になる。
- テスト容易性: 必要な機能だけをモック化できる。
- ISP (Interface Segregation Principle): 「使わないメソッドを強制されない」ようにする。
例題: インターフェース分離
interface Reader {
String read();
}
interface Writer {
void write(String data);
}
class FileStorage implements Reader, Writer {
public String read() { return "ファイルから読み込み"; }
public void write(String data) { System.out.println("ファイルに書き込み: " + data); }
}
Java👉 テスト時は必要なインターフェースだけモック化:
class DummyReader implements Reader {
public String read() { return "テストデータ"; }
}
Javaテスト容易性のメリット
- 依存を差し替え可能: DBや外部APIをモックに置き換えられる。
- 副作用を制御: ネットワークやファイルアクセスを避け、純粋なロジックだけテストできる。
- テスト速度向上: 外部依存を排除することで、テストが高速・安定。
- 責務分離: クラスが「何をするか」に集中できる。
例題で練習
例題1: DBアクセスをモック化
interface UserRepository {
void save(String user);
}
class JdbcUserRepository implements UserRepository {
public void save(String user) {
System.out.println("DB保存: " + user);
}
}
class UserService {
private final UserRepository repo;
public UserService(UserRepository repo) { this.repo = repo; }
public void register(String user) { repo.save(user); }
}
// テスト用モック
class DummyUserRepository implements UserRepository {
public void save(String user) { System.out.println("テスト保存: " + user); }
}
Java👉 テスト時は DummyUserRepository を渡すことで、DB不要でテスト可能。
例題2: JUnitでモック注入
import static org.mockito.Mockito.*;
import org.junit.jupiter.api.Test;
class UserServiceTest {
@Test
void testRegister() {
UserRepository mockRepo = mock(UserRepository.class);
UserService service = new UserService(mockRepo);
service.register("Alice");
verify(mockRepo).save("Alice"); // 呼び出し確認
}
}
Java👉 Mockito を使えばモックを簡単に作成できる。
テンプレート集
コンストラクタ注入
class Service {
private final Dependency dep;
public Service(Dependency dep) { this.dep = dep; }
}
Javaインターフェース分離
interface Reader { T read(); }
interface Writer { void write(T data); }
Javaテスト用モック
class DummyDependency implements Dependency {
public void action() { /* テスト用の簡易処理 */ }
}
Javaまとめ
- DI: 依存を外から渡すことで、テスト時にモックを注入できる。
- インターフェース分離: 必要な機能だけを切り出すことで、テストがシンプルになる。
- テスト容易性: 外部依存を排除し、純粋なロジックを素早く検証できる。
👉 練習課題として「メール送信サービス」を作り、SMTP実装とダミー実装を切り替えてテストすると、DIとインターフェース分離の効果が体感できます。
