Java 逆引き集 | 単体テストを意識した設計(DI, インターフェース分離) — テスト容易性

Java Java
スポンサーリンク

単体テストを意識した設計(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とインターフェース分離の効果が体感できます。

タイトルとURLをコピーしました