メソッドバリデーションのユニットテスト

この記事はJavaEE Advent Calendar 2013の12日目として書かれたものです。昨日は@yoshioteradaさんの「JSF + WebSocket で実装した IMAP Web メール・クライアント」でした。

11月に開催されたJJUGナイトセミナーの「おっぴろげJavaEE DevOps」のテストに関するセクションで「極力ユニットテストに寄せることで、JavaEEコンテナを使わずテストの実行速度を早くする」というお話をしました。

セッションではJPAのテストをJavaEEコンテナから切り離し、ユニットテストとして実行する方法をご紹介しましたが、時間の関係で省いたトピックをご紹介します。メソッドバリデーションのユニットテストです。

メソッドバリデーション

メソッドバリデーションはBeanValidation1.1で定義された機能の1つでJavaEE7から標準で利用できます。メソッドの引数や返り値にアノテーションを付加することで、宣言的にバリデーションを定義できるのでコードブロックは本質的なロジックの記述に専念できるようになります。

たとえばユーザー名とパスワードのnullチェックをする場合、こののようにバリデーションのコードをメソッドの先頭部分に書くことが多いのではないでしょうか。

public User create(String name, String password) {
  if(name == null || password == null) {
    throw new IllegalArgumentException("name and password must not be null");
  }
  User user = new User(name, password);
  this.userDao.persist(user);

  return user;
}

このコードをメソッドバリデーションを使って宣言的に書くと以下のようになります。

public User create(@NotNull String name, @NotNull String password) {
  User user = new User(name, password);
  this.userDao.persist(user);

  return user;
}

@NotNullはBeanValidationで標準で用意されているアノテーションで、引数がnullの場合に例外をスローします。この他に正規表現でString型をチェックする@Patternというアノテーションや、数値型のチェックを行う@Max / @Minアノテーションがある他、独自にバリデーションルールを定義したアノテーションを使うこともできます。

テストをどうするのか

冒頭でもご紹介したようにメソッドバリデーションはJavaEE7の機能として提供されているので、JavaEE7対応のコンテナを使えばそのままテストができます。しかし、コンテナの起動が早くなったとはいえ起動に少なくとも1秒以上かかる環境で繰り返しテストを実行しながら開発するのは現実的ではありません。

そこで、メソッドバリデーションの仕組みを切り出したヘルパークラスを用意することで、コンテナを使わないテストとして記述できるようにします。やっていることはメソッドの呼び出しの前後で引数と返り値のバリデーションをしているだけなので、AOPで解決するアプローチをとります。

javassistのAOPでメソッドバリデーションする

単体でメソッドバリデーションを使うには、BeanValidationで用意されているExecutableValidatorというクラスを使います。プロバイダクラスはMETA-INF/services/javax.validation.spi.ValidationProviderに定義されているクラスが使われます。GlassFish4.0、WildFly8.0の場合はHibernateValidatorが同梱されているので、クラスパスを追加せずに使うことができます。

テスト対象のクラスをjavassistでラップし、メソッド呼び出し前後でバリデーションを行うヘルパークラスを作成しました。

public class MethodValidationHelper {
  private static ExecutableValidator VALIDATOR = Validation.
    byDefaultProvider().
    configure().
    buildValidatorFactory().
    getValidator().
    forExecutables();

  @SneakyThrows
  public static T create(Class clazz) {
    ProxyFactory proxyFactory = new ProxyFactory();
    proxyFactory.setSuperclass(clazz);

    @SuppressWarnings("unchecked")
    T object = (T) proxyFactory.createClass().newInstance();

    ((ProxyObject) object).setHandler(new MethodHandler() {
      @Override
      public Object invoke(Object self, Method thisMethod, Method proceed, Object[] args) throws Throwable {
        // 引数のバリデーション
        Set> parameterViolations = VALIDATOR.validateParameters(self, thisMethod, args);
        if (!parameterViolations.isEmpty()) {
          throw new ConstraintViolationException(parameterViolations);
        }

        // メソッドの実行
        Object result;
        try {
          result = proceed.invoke(self, args);
        } catch (InvocationTargetException e) {
          // メソッド内で発生した例外はInvocationTargetExceptionでラップされている
          throw e.getCause();
        }

        // 返り値のバリデーション
        Set> returnViolations = VALIDATOR.validateReturnValue(self, thisMethod, result);
        if (!returnViolations.isEmpty()) {
          throw new ConstraintViolationException(returnViolations);
        }

        return result;
      }
    });

    return object;
  }
}

このクラスをテスト対象クラスの生成に使います。

private UserService userService = MethodValidationHelper.create(UserService.class);

@Test
public void createSuccess() {
  this.userService.create("user", "password");
}

@Test(expected = ConstraintViolationException.class)
public void createFailureByNullName() {
  this.userService.create(null, "password");
}

まとめ

このヘルパークラスを使ったアプリケーションのサンプルをGitHubにアップしておきました。おっぴろげJavaEE DevOpsでお話した内容と合わせてご利用ください。

あまり大々的に取り上げられずひっそりと標準化されたメソッドバリデーションですが、宣言的にバリデーションを記述すると、本質的なロジックと明確にコードを切り分けられるため、見通しのよいコードを書くことができるようになります。

バリデーションやJPAのテストなどをJavaEEコンテナから切り離してテストができる環境を整えることで、テンポよく気持ちのいい開発ができるようになります。ぜひお試しください。

明日は@kisさんです。

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です

次のHTML タグと属性が使えます: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>