Guiceを使ってみた

今開発しているプログラムをGuiceを使って書いてみた。そもそもGuiceを使おうと決めた理由はテストを書き易くするため。実際に使ってみた感想としては「ま〜、こんなもんか」って感じ。

だいたいの使用パターンとしては以下のような感じになると思う。Guiceの高度な機能を使うと他の人がコードを読めなくなりそうなので最低限の機能だけ使うことにする。

まずはテスト時に差し替えたいクラスのIFを切り出しておく。

public interface LoginClient {
  public boolean login(String id, String password);
}

で、実コードを実装する。

public class LoginClientImpl implements LoginClient {
  public boolean login(String id, String password){
    //DB Access..
    return true;
  }
}

次に、それを使う側のクラスを定義する。こっちはIF切らなくてもよい。

import com.google.inject.Inject;

public class Service {
  
  private LoginClient loginClient;
  
  @Inject
  public Service(LoginClient loginClient){
    this.loginClient = loginClient;
  }
  
  public String execute(){
    boolean login = loginClient.login("ken", "xxxx");
    if(login){
      return "OK";
    }else{
      return "NG";
    }
  }
}

このServiceクラスはGuiceによってnewされることを想定している。
コンストラクタに「@Inject」をつけたので、Guiceインスタンスを作るときに、LoginClientをインジェクトしておいてくれる。

なお、フィールドに@Injectをつけておけば、コンストラクタを定義しなくてもGuiceがインジェクトしておいてくれる。privateなフィールドでもOK。本当は全部フィールドインジェクションでいければ美しいんだろうけど、テストが書き辛くなる(後述)という結論に達したため、コンストラクタインジェクションを使うことにする。

次にGuiceによるDIの設定をするためのモジュールクラスを定義する。どのIFにどの具象クラスがインジェクトされるか、ってことを定義するクラス。Javaで書ける設定ファイルみたいなもんですね。

import com.google.inject.AbstractModule;

public class MyModule extends AbstractModule{
  @Override
  protected void configure() {
    bind(LoginClient.class).to(LoginClientImpl.class);
  }
}

で、最後にエントリポイントのクラスを作る。

import com.google.inject.Guice;
import com.google.inject.Injector;

public class Main {
  public static void main(String[] args) {
    Injector injector = Guice.createInjector(new MyModule());
    Service service = injector.getInstance(Service.class);
    String msg = service.execute();
    System.out.println(msg);
  }
}

最初にModuleを指定してInjectorを作っておき、それ以降はInjectorにインスタンスを作ってもらう。

まあ、ここまではいいんだ。問題ない。すっきりしててよい。
で、ここからServiceクラスのテストコードを書いて行きたいわけだ。ただし、Serviceクラスの中で使っているLoginClientImplはDBにアクセスするので、テストのときはDBにアクセスしないモックで差し替えてテストしたい。

実コードの方ではInjectorを使ってServiceを構築したわけなので、テストの場合もテスト用のModuleを使ってInjectorを作り、DBアクセス無しのLoginClientが差し込まれたServiceオブジェクトを構築したい。

するとテストコードはこんな感じになる。

import static org.junit.Assert.assertEquals;
import guicesample.LoginClient;
import guicesample.Service;

import org.junit.Test;

import com.google.inject.AbstractModule;
import com.google.inject.Guice;
import com.google.inject.Injector;

public class ServiceTestWithGuice {
  @Test
  public void testExecuteWhenLoginSuccess(){
    Injector injector = Guice.createInjector(new AbstractModule(){
      @Override
      protected void configure() {
        bind(LoginClient.class).to(LoginClientSuccess.class);
      }});
    Service service = injector.getInstance(Service.class);
    assertEquals("OK", service.execute());
  }
  
  @Test
  public void testExecuteWhenLoginFailure(){
    Injector injector = Guice.createInjector(new AbstractModule(){
      @Override
      protected void configure() {
        bind(LoginClient.class).to(LoginClientFailure.class);
      }});
    Service service = injector.getInstance(Service.class); 
    assertEquals("NG", service.execute());
  }
  
  static class LoginClientSuccess implements LoginClient{
    @Override
    public boolean login(String id, String password) {
      return true;
    }
  }

  static class LoginClientFailure implements LoginClient{
    @Override
    public boolean login(String id, String password) {
      return false;
    }
  }
}

あああ・・、ながい。複雑。
本当はLoginClientのモックは無名クラスとしてテストメソッド内で定義したいんだけど、bind().to()の引数はClassを指定する必要があり、かつGuiceの仕様により非staticな内部クラスはインジェクト対象に出来ない、という制限があるため、上記のように、ログイン成功用、失敗用のLoginClientモックをわざわざ定義した、ってわけです。めんどくせー。

あまりにもテストコードが嫌な感じなんで、テストコードはGuiceを使わず以下のように書くことにする。

import static org.junit.Assert.*;
import guicesample.LoginClient;
import guicesample.Service;

import org.junit.Test;

public class ServiceTest {
  @Test
  public void testExecuteWhenLoginSuccess(){
    Service service = new Service(new LoginClient() {
      @Override
      public boolean login(String id, String password) {
        return true;
      }
    });
    assertEquals("OK", service.execute());
  }
  
  @Test
  public void testExecuteWhenLoginFailure(){
    Service service = new Service(new LoginClient() {
      @Override
      public boolean login(String id, String password) {
        return false;
      }
    });
    assertEquals("NG", service.execute());
  }
}

まあ、だいぶすっきりしますね。

ただ、フィールドインジェクションを使っている場合は上記のような書き方はできない。とくに非privateなフィールドに対してインジェクトしている場合は、テストコードもGuiceを使うか、setterを別途用意しておくか、リフレクションを使う必要がある。privateフィールドに対するインジェクションはすっきりしていて好きなんですが。

そもそも、テスト時にオブジェクトの差し替えをしたいだけなら、DI使わずにモックライブラリ使えって感じですが。

何の話をしているのか混乱してきましたが、まーとにかく、簡潔で、理解し易く、かつテストしやすいコードになるように、うまくバランスをとらなきゃいけないですね。

追記(2010/03/21):
shinさんからのコメントより。こんな感じで書けるようです!

public class ServiceTestWithGuice {
  
  @Test
  public void testExecuteWhenLoginSuccess(){
    Injector injector = Guice.createInjector(new AbstractModule(){
      @Override
      protected void configure() {
        bind(LoginClient.class).toInstance(new LoginClient() {
          @Override
          public boolean login(String id, String password) {
            return true;
          }
        });
      }});
    Service service = injector.getInstance(Service.class);
    assertEquals("OK", service.execute());
  }
  
  @Test
  public void testExecuteWhenLoginFailure(){
    Injector injector = Guice.createInjector(new AbstractModule(){
      @Override
      protected void configure() {
        bind(LoginClient.class).toInstance(new LoginClient() {
          @Override
          public boolean login(String id, String password) {
            return false;
          }
        });
      }});
    Service service = injector.getInstance(Service.class);
    assertEquals("NG", service.execute());
  }