mike-neckのブログ

Java or Groovy or Swift or Golang

JUnit5の標準のassertとDynamicTestの用い方

JUnit5でわりと便利だと思っているのが、JUnit5に標準でついてくる Assertions#assertAll@TestFactory で返す Iterable<DynamicTest> です。


エンタープライズな現場でよく見かけるテストとして、こういうのがあるかと思います。

@Test
void firstTest() {
    final Map<Long, UserEntity> map = getUsers();

    final UserEntity u1 = map.get(1L);
    assumeTrue(u1 != null);
    assertEquals(1L, u1.getId());
    assertEquals("ユーザー1", u1.getName());
    assertEquals("test1@example.com", u1.getEmail());

    final UserEntity u2 = map.get(2L);
    assumeTrue(u2 != null);
    assertEquals(2L, u2.getId());
    assertEquals("ユーザー2", u2.getName());
    assertEquals("test2@example.com", u2.getEmail());

    final UserEntity u3 = map.get(3L);
    assumeTrue(u3 != null);
    assertEquals(3L, u3.getId());
}

private Map<Long, UserEntity> getUsers() {
    return SampleCodesTest.mapOf(
                kv(1L, new UserEntity(1L, "ユーザー1", "test1@example.com", "password1")),// 
                kv(3L, new UserEntity(3L, "ユーザー3", "test3@example.com", "password3"))//
        );
}

なるべく一つのテストメソッドには一つのアサーションにするべきなのはわかってはいるけれど、どうしても各プロパティの値を調べたいし、 equals を実装して簡単にアサーションしたいけど何らかの制約により equals が実装できないので仕方なくプロパティの個数だけアサーションをするようなケースです。

この複数回アサーションを実行するのがまずいのは、アサーションが失敗するケースがいくつかある場合に、最初の一つだけが落ちて、残りのアサーションが実行されないので、何が落ちているのかがわからないということにあります。実際上に書いたテストも途中でabortされていて、何が通って何が落ちているのかがわかりません。

そこでJUnit5では、複数のアサーションを引数にとって、すべてのアサーションを実行するアサーション Assertions.assertAll が定義されています。それでは、上記のテストを assertAll を用いて書き換えてみます。

@Test
void secondTest() {
    final Map<Long, UserEntity> map = getUsers();

    final UserEntity u1 = map.get(1L);
    final UserEntity u2 = map.get(2L);
    final UserEntity u3 = map.get(3L);

    assertAll(
            () -> assumeTrue(u1 != null),
            () -> assertEquals(1L, u1.getId()),
            () -> assertEquals("ユーザー1", u1.getName()),
            () -> assertEquals("test1@example.com", u1.getEmail()),
            () -> assumeTrue(u2 != null),
            () -> assertEquals(2L, u2.getId()),
            () -> assertEquals("ユーザー2", u2.getName()),
            () -> assertEquals("test2@example.com", u2.getEmail()),
            () -> assumeTrue(u3 != null),
            () -> assertEquals(3L, u3.getId())
    );
}

さて、これでもう安心と言えるかというと、実はそんなことはありません。なぜかというと、 assertAll の中にテストをabortするものが入っていると、すべてを実行してくれなくなるためです。 assertAll は一つでも落ちるものがある場合にはアサーションが失敗になるように複数のアサーションをまとめてくれる機能ですが、一つでもabortがあると、途中で止まってしまい、現在のテストの状態がわからなくなります。上記のテストも途中でabortされてしまい、最初のパターンと何ら変わるところがありません。

そこで、用いるのが、これまたJUnit5の標準に入っている DynamicTest@TestFactory です。

DynamicTest@TestFactory は動的にテストを作ると説明されていることがありますが、複数のアサーションをまとめたい場合に適用するのが、最適な活用パターンではないかと思います。

では、先ほどのテストを書き換えてみましょう。

@TestFactory
List<DynamicTest> thirdTest() {
    final Map<Long, UserEntity> map = getUsers();
    final List<DynamicTest> tests = new ArrayList<>();

    final UserEntity u1 = map.get(1L);
    tests.add(dynamicTest("ユーザー1",//
            () -> assertAll(
                    () -> assumeTrue(u1 != null),
                    () -> assertEquals(1L, u1.getId()),
                    () -> assertEquals("ユーザー1", u1.getName()),
                    () -> assertEquals("test1@example.com", u1.getEmail())
            )));

    final UserEntity u2 = map.get(2L);
    tests.add(dynamicTest("ユーザー2",//
            () -> assertAll(
                    () -> assumeTrue(u2 != null),
                    () -> assertEquals(2L, u2.getId()),
                    () -> assertEquals("ユーザー2", u2.getName()),
                    () -> assertEquals("test2@example.com", u2.getEmail())
            )));

    final UserEntity u3 = map.get(3L);
    tests.add(dynamicTest("ユーザー3",//
            () -> assertAll(
                    () -> assumeTrue(u3 != null),
                    () -> assertEquals(3L, u3.getId())
            )));

    return tests;
}

このテストを実行すると、「ユーザー1」と「ユーザー3」というテストが通っていて、「ユーザー2」というテストがabortされている状態がわかります。これで現在のテストの状態がわかるようになりました。


という感じで、何らかの理由により複数のアサーションをせざるを得ない時に、JUnit5の機能を用いてテストを整理するとテストの状態がよくわかるようになるという話でした。

ジェネリクス勉強会に行ってきた件

表題のとおりです。

connpass.com

おっさんなのにわりと知らないことも多くて、不勉強を実感しました。

HaskellGHC.Genericsはあまりよくわかってないので、ちゃんとやるべきですね…

なお、当日のツイートは #ジェネリクス勉強会 since:2017-06-24 until:2017-06-25 で検索すれば見つかると思います。


で、 @kmizu さんがした、抽象型でジェネリクスの主要な概念を実装するという 発表内容 が面白く、高階型もエミュレートできるとのことでしたので、家に帰ってから発表されてた内容を思い出しつつ自分でも書いてみました。

題材としては Functor(二番煎じ) + Maybe Functor(実装が簡単)を用いています。


まず関数を定義します。

trait Fun1 {
  type I
  type O

  def apply(i: I): O
}

次に高階型を定義します。(発表中に気づいて、「なるほど」と思った)

trait Hk1 {
  type E
}

次に Functor を定義します

trait Functor {self =>
  type F <: Hk1
  type A
  type B

  //noinspection ApparentRefinementOfResultType
  def map(o: F {type E = self.A}, f: Fun1 {type I = self.A; type O = self.B}): F { type E = self.B }

  def map(f: Fun1 {type I = self.A; type O = self.B}): Fun1 {
    type I = F { type E = self.A }
    type O = F { type E = self.B }
  } = new Fun1 {
    override type I = F { type E = self.A }
    override type O = F { type E = self.B }

    override def apply(input: F { type E = self.A }): F { type E = self.B } = self.map(input, f) 
  }  
}

ちなみに僕は初めて trait Foo {self => と書いたときの self が何を指しているのかがわかりました

@kmizuさんの発表では最初の map 関数だけ定義されていましたが、Haskellfmap f を新たに関数として用いているパターンを見かけるので、 map(f: A => B): F[A] => F[B] となるような関数も定義してみました(完全に自己満足です)。

で、 Maybe 型を定義します。

trait Maybe extends Hk1 {
  def getValue: E
}

とりあえず、具体的に IntMaybe を定義します

object NothingInt extends Maybe {
  override type E = Int
  override def getValue: Int = throw new NoSuchElementException("this is Nothing")
}

case class JustInt(value: Int) extends Maybe {
  override type E = Int
  override def getValue: Int = value
}

同様に StringMaybe も定義します

object NothingString extends Maybe {
  override type E = String
  override def getValue: String = throw new NoSuchElementException("this is Nothing")
}

case class JustString(value: String) extends Maybe {
  override type E = String
  override def getValue: String = value
}

ジェネリクスを使えば Just[A]Nothing を定義すれば済みますが、型1つずつに型をつくらないといけない辺りが実用性が…と感じられますが、コンピューターが自動でこのようなボイラープレートなコードを生成するのであれば気にならないと思います(コンパイル時に型を決定してこれと同様なバイナリを生成するなどの作戦はあるかと思う)。

そして Functorインスタンスを作ります

object MaybeIntToStringFunctor extends Functor {
  override type F = Maybe
  override type A = Int
  override type B = String

  override def map(o: Maybe { type E = Int }, f: Fun1 { type I = Int ;type O = String }): Maybe { type E = String } =
    o match {
      case NothingInt => NothingString
      case JustInt(v) => JustString(f(v))
    }
}

では、実際にこれを使ってみたいと思います。

// Fun1 を作るのが面倒なので、通常の関数から Fun1 を作る関数を作る
def fun[A,B](f: A => B): Fun1 { type I = A; type O = B } = new Fun1 { type I = A; type O = B; override def apply(i: I): O = f(i) }

// とりあえず、 Functor のインスタンスを implicit な変数にあれする
implicit val maybeIntToStringFunctor = MaybeIntToStringFunctor

// map という関数を作って Functor の map 関数を呼べるようにする
def map(f: Fun1{ type I = Int; type O = String})(implicit tci: Functor { type A = Int; type B = String }): Fun1{ type I = tci.F { type E = Int }; type O = tci.F { type E = String } } = new Fun1 {
  type I = tci.F { type E = Int }
  type O = tci.F { type E = String }
  override def apply(i: I): O = tci.map(i,f)
}

// map関数にFun1を適用して Maybe をmappingする関数を作る
val intToStringMapper = map(fun(i => "the value is " + i))

// ↑の関数に JustInt(10) を適用する
intToStringMapper(JustInt(10)) // -> JustString("the value is 10")

// ↑の関数にNothingInt を適用する
intToStringMapper(NothingInt) // -> NothingString

f:id:mike_neck:20170625010223p:plain

という感じでScalaよくわかってませんが作れました


この Functor ですが、 String から Int にマップする場合にはまた別の Functorインスタンスを作る必要があり、この形で人間がコードを書くのは若干辛いものがありました…(本当はモッを作ろうとしたけど、やめた)

daabの開発をKotlinでできるようにする

今いる会社の提供するdirectというチャットサービスではボットの開発ができて、そのsdkdaab(direct agent assist bot)というらしい。

で、daabチームが専修大学ハッカソンを共催するということで、僕も自分の会社のボットくらい作れないと恥ずかしいなと思ったので1日だけ参加してきました。


daabはhubotをdirect用に改造した感じのもので、次のようなプログラムで簡単にボットが記述できます。

module.exports = robot => {
    robot.hear(/^today/i, res => {
        var now = new Date();
        res.send(dateFormat(now, "yyyy/mm/dd"));
    });
};

ところが、困ったことにどういう関数があるのか、何を引数に取るのか、ドキュメントを漁ってもいまいちわかりません。

ドキュメントを漁ってるうちに、Typescriptでhubotを書きたい人なら型定義ファイルを作っているに違いないと思い立って、探してみたら下の記事を見つけました。

qiita.com


この記事にかかれていた型定義をそのままコピってTypescriptでdaabを書いてもいいんだけど、もうひとひねり入れて、ts2ktにかけてKotlinの型情報に変換してdaabをKotlinで書けるようにしてみました。

ts2ktをしてできた型情報(一部省略/修正)

@file:Suppress("INTERFACE_WITH_SUPERCLASS", "OVERRIDING_FINAL_MEMBER", "RETURN_TYPE_MISMATCH_ON_OVERRIDE", "CONFLICTING_OVERLOADS")
@file:JsQualifier("hubot")
package hubot

external interface Robot {

    fun hear(regex: RegExp, callback: ((res: Response) -> Unit)? = definedExternally /* null */)
    fun hear(regex: RegExp, options: Function<*>, callback: ((res: Response) -> Unit)? = definedExternally /* null */)
    fun hear(regex: RegExp, options: Json, callback: ((res: Response) -> Unit)? = definedExternally /* null */)

}

external interface Response {

    fun send(vararg strings: String)

}

ボット部分のコードは見事に綺麗なKotlinで書けました。

package com.example

import hubot.Response
import hubot.Robot
import kotlin.js.RegExp

val app: (Robot) -> Unit = { robot: Robot ->
    robot.hear(RegExp("now")) { res: Response ->
        res.send(dateTime())
    }
}

kotlin-jsはkotlinc-jsではなく、gradleでコンパイルしました。なお、 build.gradle はこんな感じ

buildscript {
    ext.kotlin_version = '1.1.2-4'
    repositories {
        mavenCentral()
    }
    dependencies {
        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
    }
}

apply plugin: 'kotlin2js'

dependencies {
    compile "org.jetbrains.kotlin:kotlin-stdlib-js:$kotlin_version"
}
compileKotlin2Js {
    kotlinOptions.outputFile = "${projectDir}/daab/scripts/lib/daab.js"
    kotlinOptions.moduleKind = "commonjs"
    kotlinOptions.sourceMap = true
}
task generateAppJs {
    def jsFile = file("$projectDir/daab/scripts/application.js")
    outputs.file jsFile
    doLast {
        def content = """let kotlin = require("kotlin");
let daab = require("./lib/daab");
module.exports = daab.com.example.app;
"""
        jsFile.write(content, 'UTF-8')
    }
}
tasks.compileKotlin2Js.dependsOn tasks.foreverIgnore
tasks.compileKotlin2Js.finalizedBy tasks.generateAppJs

実行した結果は次のとおり、見事に動かせました

f:id:mike_neck:20170611113817p:plain


なお、気をつけるところがいくつかあって、KotlinのAPIJVM環境のプログラムを書く場合のAPIと若干異なっているところです。例えば Date 型はありますが、 java.util.Date ではなくて、 kotlin.js.Date になっていて getTime() メソッドも Long ではなく、 Double を返すなど異なっています。

今回の成果をもう少し汎用的にして、かつ、同僚のエンジニアにAPIを確認して型情報を明確にした上でgradleプラグインを出そうかなと思っています。

JJUG CCC 2017 Spring 参加メモ

表題の通りJJUG CCC 2017 Springに行ってきました。参加したセッションのメモです。

www.java-users.jp


エンプラ開発におけるレガシーアプリケーションの巻取りとモジュール分割の戦い

  • 既存のレガシーアプリケーションにアプリケーションを追加する案件から、機能を巻き取っていく過程の話
  • 普段勉強会などや事例集などで目や耳にする事例をどのように自分たちのプロジェクトに適用していくか普段から考え、備えておくことが重要とのこと

www.slideshare.net

データ履歴のためのテンポラルデータモデルとReladomoの紹介

  • システムにおける履歴データと事実としての履歴データを扱うためのReladomoの紹介
  • ゴールドマン・サックスが公開しているOSS
  • サンプルは見せてもらったけど、実施に自分で触ってみないことにはなんとも言えないかな

www.slideshare.net

文型さえおさえれば英語を読む力は上がる!

  • SV/SVC/SVO/SVOC/SVOOといった五文型(懐かしい)を抑えれば、英語を雰囲気で読むことはなくなるという話
  • Springのドキュメントが癖のある文章が多いそうなので、これが読めるようになると英語に強くなれるとのこと
    • 癖のある文章を読めるようになることで英語に強くなれるとは僕は思いませんが…

speakerdeck.com

Javaで実装して学ぶOAuth2.0!

  • JavaのOAuth2の参照実装であるOltuを通してOAuth2を理解する話
  • 大きなプレゼンでのデモには魔物が住むというのがよくわかった
  • OAuth2を抑えるにはまず正確に用語を理解することと、RFCを読むことが重要とのこと
  • 今回Spring Security OAuth2を使わなかった理由は、Springで発表する人が多くなってきてSpring一色になってしまうから

speakerdeck.com

Java8プログラミングベストプラクティス + きしだが働いているかどうかIDEのメモリ使用状況から機械学習で判定する

  • IDEのメモリ使用状況のログをフーリエ解析して、それを機械学習して、働いているかどうかを判定できるようにする話
    • オチが秀逸
  • Java8のベストプラクティス + Java7以前でも使えるベストプラクティス

思ったほど怖くない!Haskell on JVM超入門

  • JVMで動くHaskellのFregeとEtaの紹介
  • 純粋な世界でモナドがなぜ必要になるのかを詳しく説明
    • モッがこわいと言う人はこの資料を見たほうがよいかもしれない
  • FregeにおけるJavaオブジェクトの利用方法
  • EtaにおけるJavaオブジェクトの利用方法

www.slideshare.net


懇親会ではLINEさんがスシスポンサーをして、寿司職人さんがいる寿司がふるまわれました。

LTをしてきたのですが、元からネタがなかったので正直ウケませんでした(資料は作ってる途中)


今回はdoorkeepersでキャンセル待ちが発生するほど参加者が多くなってました。(てらださんのツイートによると参加者は1032人ほどだったそうです。)

以上