mike-neckのブログ

Java or Groovy or Swift or Golang

JUnit5のNestedクラスを使ってテスト条件をクラスの構造に閉じ込める(テスト小ネタ Advent Calendar 2017 6日目)

f:id:mike_neck:20171116211230p:plain

JUnit4まででは増えすぎたテストを整理するために、スタティックなメンバークラスを作って整理してきたかと思います。

@RunWith(Enclosed.class)
public class Foo {
  public static class Bar {
    @Test
    public void baz() {}
  }
}

JUnit5 ではネストしたクラスを用いる場合、 スタティックでないメンバークラスを用いるようになりました。

class Foo {
  class Bar {
    @Test
    void baz() {}
  }
}

このような構造のクラスの場合、メンバークラスからルートにあるクラスのフィールドにアクセスできます。

class Foo {
  private String name;
  @BeforeEach
  void setup() {
    this.name = "foo";
  }
  @Nested
  class Bar {
    @Test
    void test() {
      assertEquals("foo", name); // succeed
    }
  }
}

これを応用して、テストの条件をクラス構造として表現します。

例えば、次のようなサービスクラスのメソッドがあって、モックを用いたテストを行います。

public class ReportServiceImpl imlements ReportService {
  private final TimeRepository timeRepository;
  private final GameDao gameDao;
  private final UserExternalRepository userExternalRepository;

  // これをテストしたい
  public List<UserScore> getDailyRanking(final LocalDate date) {
    if (!timeRepository.isBeforeToday(date)) {
      return Collections.emptyMap();
    }
    final List<UserScore> userScoreList = gameDao.dailyUserScoreList(date);
    if (userScoreList.isEmpty()) {
      return Collections.emptyMap();
    }
    final Set<UserId> users = userScore.stream().map(UserScore::getUserId).collect(toSet());
    final Collection<UserId> premiumUsers = userExternalRepository.findPremiumUsers(users);
    if (premiumUsers.isEmpty()) {
      return Collections.emptyMap();
    }
    return userScoreList.stream()
        .filter(s -> premiumUsers.contains(s.getUserId()))
        .sorted((l,r) -> Long.compare(l.getScore(), r.getScore()))
        .collect(toList());
  }
}

JUnit4であると、このようなメソッドのテストは次のように条件の記述が占めるようになります。

@RunWith(MockitoJUnitRunner.class)
public class ReportServiceGetDailyRankingTest {
  @Mock TimeRepository timeRepository;
  @Mock GameDao gameDao;
  @Mock UserExternalRepository userExternalRepository;
  @InjectMock ReportServiceImpl service;

  @Test //1
  public void 指定日のスコアがない場合の結果は0件() {
    when(timeRepository.isBeforeToday(any())).thenReturn(true);
    when(gameDao.dailyUserScoreList(any())).thenReturn(Collections.emptyList());

    final List<UserScore> actual = service.getDailyRanking(LocalDate.now());

    assertTrue(actual.isEmpty());
  }

  @Test //2
  public void 指定日が実行日と同じ場合の結果は0件() {
    when(timeRepository.isBeforeToday(any())).thenReturn(false);

    final List<UserScore> actual = service.getDailyRanking(LocalDate.now());

    assertTrue(actual.isEmpty());
  }

  @Test //3
  public void プレミアムユーザーがいない場合の結果は0件() {
    when(timeRepository.isBeforeToday(any())).thenReturn(true);
    when(gameDao.dailyUserScoreList(any())).thenReturn(Arrays.asList(score1, score2, score3));
    when(userExternalRepository.findPremiumUsers(anyIterable())).thenReturn(Collections.emptySet());

    final List<UserScore> actual = service.getDailyRanking(LocalDate.now());

    assertTrue(actual.isEmpty());
  }
}

メソッドやクラスの設計が適切な状態であれば、メソッドが依存するモジュールの数も少なく、モックの記述量も少なくなるため、これでも良いかもしれません。しかし、現実的にはメソッドの分割が不十分で依存するモジュールも多いため、モックの記述量もばかにならないようなテストがあったりします。また、幾つかのモックの記述が重複するため、共通できるものを @Before で定義しすぎた結果、 UnnecessaryStubbingException という例外が出て、 MockitoJunitRunner ではなく MockitoJunitRunner.Silent を使わざるを得なくなるのではないかと思います。基本的な使い方が変わらないJUnit5でも言えてくるのではないかと思います。

そこで、最初に書いた メンバークラスがスタティックでないことを利用します。

ルートにあるクラスから、条件を積み重ねていった入れ子クラスを作っていって、それをテストの事前条件にします。

例えば、先の例では 1個目のテストと3個目のテストは timeRepository の条件を共有しているので、それらのテストを同じクラスのメンバークラスとして記述するようにします。また、2個目のテストの timeRepository の条件は 1個目と3個目とは異なるので、それらとは異なるクラスのメンバークラスに記述します。また、1個目と3個目のテストは gameDao については異なる条件ですので、これらは別々のクラスに所属します。

@DisplayName("getDailyRankingのパラメーターが")
class ReportServiceGetDailyRankingTest {
  private TimeRepository timeRepository = mock(TimeRepository.class);

  @DisplayName("本日の場合")
  @Nested
  class TodayOrAfter {
    private ReportService service;

    @BeforeEach
    void setup() {
      when(timeRepository.isBeforeToday(any())).thenReturn(true);
      service = new ReportServiceImpl(timeRepository, mock(GameDao.class, mock(UserExternalRepository.class)))
    }
    @DisplayName("件数は0件")
    @Test
    void test() {
      final List<UserScore> actual = service.getDailyReport(LocalDate.now());
      assertTrue(actual.isEmpty());
    }
  }

  @DisplayName("前日の場合に")
  @Nested
  class BeforeToday {
    private GameDao gameDao = mock(GameDao.class);

    @BeforeEach
    void setup() {
      when(timeRepository.isBeforeToday(any())).thenReturn(false);
    }

    @DisplayName("プレイしたプレイヤーがいないときは")
    @Nested
    class NoPlayer {
    private ReportService service;

      @BeforeEach
      void setup() {
        when(gameDao.dailyUserScoreList(anyIterable())).thenReturn(Collecitons.emptySet());
        service = new ReportServiceImpl(timeRepository, mock(GameDao.class, mock(UserExternalRepository.class)))
      }

      @DisplayName("件数は0件")
      @Test
      void test() {
        final List<UserScore> actual = service.getDailyReport(LocalDate.now());
        assertTrue(actual.isEmpty());
      }
    }

    // 以下略
  }
}

このテストの書き方のメリットはテストクラスの構造によってテスト条件が明確になるので、テストのメンテナンスなどの際に条件が理解しやすく修正箇所が小さくなるということです。デメリットはネストが深くなってしまってIDEやスタイルの設定によっては読みづらくなるということです。ただし、このネストの深さについては、ここでの議論の入り口にある、クラス・メソッドの分割ができていないという状態を改善することで修正できますし、そうするべきだと思っています。

Kotlin で groovy.lang.Closure のインスタンスを生成・取得する(G* Advent Calendar 2017 6日目)

G* Advent Calendar 2017 の 6日目は groovy.lang.Closureインスタンスを Kotlin で生成・取得する方法です。

f:id:mike_neck:20150917235151p:plain
Groovy!

3分くらいで読み終われます。


groovy.lang.Closureインスタンスを Kotlin で生成・取得する方法です。Kotlin コードの中で Closure を作って Groovy に渡すということを指しています。


生成方法

早速ですが生成方法です。

ある A というクラスがあって、 Closure の中で用いるプロパティを A から取得したい場合は次のような関数を書きます。

fun <A> closure(a: A, cl: A.() -> Unit): Closure<Unit> = object: Closure<Unit>(a) {
  fun doCall() = this@closure.cl()
}

ちょっと欲張りに A というクラスで宣言した Closure で、中で用いるプロパティを B から取得しつつ、 C を引数で受け取って、 D を返す Closure を生成したい場合は次のような関数を書きます。

fun <A, B, C, D> closure(declaredAt: A, receiver: B, cl: B.(C) -> D): Closure<D> = object: Closure<Unit>(this) {
  init {
    delegate = receiver
  }
  fun doCall(c: C): D = this@closure.cl(c)
}

試してみる

最後の欲張りな記述方法を用いた例を書いてみます。次のような Kotlin コードを書きます。

object ClosureCreation {
  @JvmStatic
  fun <A> stringToIntClosure(declaredAt: A, delegate: Supplier<String>): Closure<Int> =
      closure<A, Supplier<String>, String, Int>(declaredAt, delegate) { s -> "${get()} $s".length }
}

この stringToIntClosure 関数を呼ぶと java.util.function.Supplier を引数にとって、kotlin.Int(実質は int) が返ってくる groovy.lang.Closure が返ってきます。

そして、こちらが利用するコードになります。

class ClosureCreationTest {
  @Test
  void 'Kotlin で 作った closure を Groovy で扱う'() {
    def closure = ClosureCreation.stringToIntClosure(this, [get: { 'Groovy' }] as Supplier<String>)
    assert closure('Calling closure made by Kotlin.') == 'Groovy Calling closure made by Kotlin.'.size()
  }
}

こちらは試していただければテストが通ることがわかるかと思います。


仕組み

Groovy の Closurejava.util.concurrent.Callable インターフェース と Runnable インターフェースを実装した抽象クラスになっています。

f:id:mike_neck:20171202144126p:plain

そして、それぞれの実装は次のようになっています。

public void run() {
  call();
}
public V call() {
  final Object[] NOARGS = EMPTY_OBJECT_ARRAY;
  return call(NOARGS);
}
public V call(Object... args) {
  return (V) getMetaClass().invokeMethod(this, "doCall", args);
}

invokeMethod という名前から想像がつく通り、最終的には doCall メソッドを動的に取得して実行します。したがって、欲しいパラメーターの型と戻り値の型をつかった doCall メソッドを object 式で作れば、 Groovy でなくても Closureインスタンスを生成できるという事になります。

また、先程までの説明ではプロパティは〜〜から取得するという説明をしましたが、これを実現するのが Closuredelegate です。何かしらのメソッド、フィールド(プロパティ)を Closure 内で取得する時の取得元になります。これは Kotlin のレシーバーつき関数(Foo.(Bar) -> Baz)のようなものです。Gradleのドキュメントなどで Closure を受け取るメソッドについては、たいてい 〜〜 is passed as the closure's delegate のように書かれていたりします。

応用

先日ブログに書いた、 junit-starter-plugin においても、ここで書いた変換を行っています。

mike-neck.hatenadiary.com

gradle-plugin-publish-plugin の DSL にて PluginBundleExtension を介して gradle plugin の id(plugins ブロックで指定するやつ)と displayName を設定しますが、PluginBundleExtensions 内部の plugins ブロックの引数が groovy.lang.Closure であるため、Kotlin の関数・レシーバーつき関数ではここを記述できないため、ここに用いた変換用の関数を使っています。

おわり

Spring Fest 2017 に行ってきた

記憶を頼りに参加した勉強会についてまとめておくだけ。

2017/11/24 開催の Spring Fest 2017 に参加しました。

springfest2017.springframework.jp

会社ではSpringを使っていないJava EEを改造したフレームワークを利用していますが、独自フレームワークは新しく入るメンバーにはハードルが高くなることもあり、なるべく普及している技術を取り入れておきたいこともあるので、出社扱いでの参加となりました。参加したセッションは以下の通り。


What's New in Spring

Spring5/Spring Boot2 に関する状況の報告、デモのセッション。丁寧にまとめられており、あとで資料を参照するだけでも現在のSpringをめぐる状況の確認になります。セッションの途中でですが、同時通訳の人は2人いて(これは前から知ってる)、10分で交代することに気づきました。

Introduction to Spring WebFlux

@making さんによる Web Flux の発表。事前に超早口でやると宣言していましたが、わりとついていけたと思います(理解しているとは言っていない)。結構大量なリクエスト(100人からとっかえひっかえ来る)も10個のスレッドで捌けるなど、Web Fluxの利点などが紹介されていました。ただし、RDBMSにつなぐようなシステムでは、jdbcがまだブロッキングであるため、あまり効果はないとのことで、jdbc2の登場が待たれるとのことです。(ただし、jdbc2はReactiveベースではないらしい)

Wagby R8 と Spring の関係

Wagby さん提供のランチセッション。Wagby の Spring移行における課題や状況などが報告されました。WagbyでのJSPサポート(利用しているユーザーが多い)の中止宣言をどうするか、複数タブを実現するためにセキュリティ部分をSpring Securityを組み込みたい一方で既存のロールなどの作り込みがSpringではまだサポートされていないのでどうしようかといった、古くから開発しているアプリケーションへの最新技術の取り込み方など参考になるセッションでした。

Spring Security 5 解剖速報

Spring Security 5 に関する情報共有のセッション。Spring Security に関する動向を具体的なサンプルコードなどとともに説明されており、Spring Securityの情報整理に参考になるセッションでした。Spring Security の OAuth2 リソースサーバー/認証サーバーは 5.1 を待たないと出てこないようで、5.1が待てない場合は既存の Spring OAuthを使うしかなさそうです。

ドメイン駆動設計のためのの上手な使い方 Spring

増田さん の Spring の利用法などのセッション。前半はドメイン駆動設計を中心に、後半はSpringとドメイン駆動設計との関連や各種プロダクトをどのように活用しているかという実践的な話。だいたい以前に聞いた話と同じ感じでした。

最近、僕はTwitterにてprivateメソッドいらないなと言ったわけですが、まあ理由としてはprivateメソッドとして切り出されたメソッドの処理内容を観ていると、privateメソッドを作ったというのは新たな関心分野を発見したことなのではないかと考えたからです。そして、そのような関心を表すデータ等を集めれば新しいクラスが出来上がるわけで、そのクラスのpublicメソッドとしてprivateに切り出されたメソッドは構成されるべきだったのだろうと思ったわけです。

と何故か処理の配置をどこにするべきかという話がこのセッションの感想として語られたのですが、ドメインを深く理解していくというのはこのような観察・発見をコードとして表して、フィードバックをふたたびコードに返していくことなのかなぁとぼんやり思っていました。

日本一やさしく説明する予定のマイクロサービス入門

長谷川さんのセッション。へりくだって、日本一「がっかりする」マイクロサービス入門などと言っていましたが、表面的な説明ではなく、これまでのソフトウェア開発・運用などの経緯を含めた視点でまとめられていて、非常に面白い発表だと思いました。

Spring と TDD

タグバンカーズさんによる発表。Springのテストでテスト対象に対してどのようなアノテーションを使ってテストしていくかということがまとめられていて、Springのテストに詳しくない僕にはカタログとして価値のある発表でした。ただひとつ残念なことはTDDの要素がまったくなかったことでした。

SpringでOAuth 2.0 OpenID Connect 1.0 を使う

うらがみさん がダラダラと(本人談) Spring をつかって OAuth2.0 と OpenID Connect 1.0 の機能を提供するアプリケーションの書き方を紹介するセッション。うん、わりとだれてた。


懇親会

昨年の懇親会はほとんど食べられなかったので、しっかりと食べました。こざけさんの会社の 若者 とお話できたのが印象的でした。

Gradle kotlin DSL の便利機能

f:id:mike_neck:20171113232305p:plain

Gradle のスクリプトを Kotlin で書いていると、タスクが自動的に二つ足されていることに気づいたので、それを使ってみたら、わりと便利だった。


kotlinDslAccessorsReport タスク

現在のプロジェクトが利用できる extension と convention のKotlin 実装コードを表示するタスク.

表示内容からは、現在のプロジェクトにて Kotlin DSL の中で呼び出すことのできるAPIの関数名やパラメーターの型がわかる.

例えば先日リリースしたこちらのプラグインを適用した状態で、 kotlinDslAccessorsReport タスクを実行する.

mike-neck.hatenadiary.com

(スクリプト本体)

plugins {
    id("org.mikeneck.junit.starter.library") version("5.0.2")
}
repositories {
    mavenCentral()
}

(出力内容(一部抜粋))

/**
 * Configures the [junitPlatform][org.junit.platform.gradle.plugin.JUnitPlatformExtension] project extension.
 */
fun Project.`junitPlatform`(configure: org.junit.platform.gradle.plugin.JUnitPlatformExtension.() -> Unit): Unit =
    extensions.configure("junitPlatform", configure)

/**
 * Retrieves the [junit][org.mikeneck.junit.starter.JunitExtra] project extension.
 */
val Project.`junit`: org.mikeneck.junit.starter.JunitExtra get() =
    extensions.getByName("junit") as org.mikeneck.junit.starter.JunitExtra

/**
 * Configures the [junit][org.mikeneck.junit.starter.JunitExtra] project extension.
 */
fun Project.`junit`(configure: org.mikeneck.junit.starter.JunitExtra.() -> Unit): Unit =
    extensions.configure("junit", configure)

/**
 * Adds a dependency to the 'api' configuration.
 *
 * @param dependencyNotation notation for the dependency to be added.
 * @param dependencyConfiguration expression to use to configure the dependency.
 * @return The dependency.
 *
 * @see [DependencyHandler.add]
 */
inline
fun DependencyHandler.`api`(
    dependencyNotation: String,
    dependencyConfiguration: ExternalModuleDependency.() -> Unit): ExternalModuleDependency =
    add("api", dependencyNotation, dependencyConfiguration)

出力内容例の一番最後の部分から、この java-library プロジェクトで 依存ライブラリーを追加する際には DependencyHandler のブロック内で api という関数に 文字列で依存ライブラリーの文字列表現とその設定用の レシーバー付き関数を渡せばよいということがわかる.

他にも、先日のプラグインの記事には書かなかった(つまり隠れコマンド的な) junit という extension の存在も見破られている.(なお、この extension は configure できないのだが…)


kotlinDslAccessorsSnapshot タスク

こちらも、 kotlinDslAccessorsReport とほぼ同様な感じで、現在のプロジェクトで利用可能な conventions / extensions / configurations の一覧を json 形式(ファイル名は project-schema.json)で gradle ディレクトリーの下に出力する.

(出力内容例)

{
    ":": {
        "conventions": {
            "base": "org.gradle.api.plugins.BasePluginConvention",
            "java": "org.gradle.api.plugins.JavaPluginConvention"
        },
        "configurations": [
            "api",
            "apiElements",
            "testRuntimeOnly"
        ],
        "extensions": {
            "ext": "org.gradle.api.plugins.ExtraPropertiesExtension",
            "defaultArtifacts": "org.gradle.api.internal.plugins.DefaultArtifactPublicationSet",
            "junit": "org.mikeneck.junit.starter.JunitExtra",
            "gradlePlugin": "org.gradle.plugin.devel.GradlePluginDevelopmentExtension"
        }
    }
}

ただ、こちらの出力は何に使うのかイマイチわからなかった…