mike-neckのブログ

Java or Groovy or Swift or Golang

JUnit5入門(1) - テストクラスの作成とテストの実行

年末にかけてJUnit5(junit-jupiter)をいじったのでまとめ。

使い方的な話はQiitaにある記事のほうが詳しいかもしれない…

qiita.com

qiita.com

qiita.com

qiita.com


JUnit5ライブラリーの導入

テストコンパイルスコープにjunit-jupiter-api、テストランタイムにjunit-jupiter-engineを用いるようにする。

build.gradle
repositories {
  mavenCentral()
  jcenter()
}

dependencies {
  testCompile "org.junit.jupiter:junit-jupiter-api:5.0.0-M3"
  testRuntime "org.junit.jupiter:junit-jupiter-engine:5.0.0-M3"
}
pom.xml
<dependency>
  <groupId>org.junit.jupiter</groupId>
  <artifactId>junit-jupiter-api</artifactId>
  <version>5.0.0-M3</version>
  <scope>test</scope>
</dependency>
<dependency>
  <groupId>org.junit.jupiter</groupId>
  <artifactId>junit-jupiter-engine</artifactId>
  <version>5.0.0-M3</version>
  <scope>test</scope>
</dependency>

JUnit5のテストクラスとテストメソッドの作り方

JUnit4とほとんど変わらない。プレーンなクラスを作って、 @Test アノテーションをテストメソッドに付加するだけで実行してくれる。

サンプルコード
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;

@Slf4j
public class SimpleTest {

    @Test
    void firstTest() {
        log.info("1st test");
    }
}

@Test アノテーションを付与するメソッドは、staticメソッドではなく、またprivateなメソッドでない必要がある。

実行結果
1 01, 2017 12:55:22 午後 org.junit.platform.launcher.core.ServiceLoaderTestEngineRegistry loadTestEngines
情報: Discovered TestEngines with IDs: [junit-jupiter]
12:55:28.796 [INFO  com.example.ex1.SimpleTest] - 1st test

コードサンプル


JUnit5の実行方法

IDEからの実行

IntelliJ IDEA

デフォルトの状態でJUnit5を実行できるようになっている。なお、実行方法はこれまでと変わらない。

f:id:mike_neck:20170101154113p:plain

テストクラスの左側にある実行ボタンをクリックして、メニューから「Run クラス名 」を実行する。

f:id:mike_neck:20170101154237p:plain

f:id:mike_neck:20170101154313p:plain

Eclipse

知らない…ごめん…

Bug 488566 - [JUnit][JUnit 5] Add support for JUnit 5を読むかぎり、Eclipse 4.7 M4にてJunit5のサポートがあるようです。

Netbeans

知らないし、ググっても出てこないけど、console-launcherは単なるjavaアプリケーションなので必要なライブラリーをclasspathに指定しつつ、console-launcherを起動すれば実行できる。

Gradleからの実行

Gradleにjunit-platformプラグインを導入して、 junitPlatformTest タスクを実行するとテストを実行できる。

build.gradle
buildscript {
  repositories {
    mavenCentral()
  }
  dependencies {
    classpath "org.junit.platform:junit-platform-gradle-plugin:1.0.0-M3"
  }
}

apply plugin: 'java'
apply plugin: 'org.junit.platform.gradle.plugin'
実行結果

junitPlatformTest は長いので jPT で指定することもできる。

$ gradle jPT
:compileJava
:processResources
:classes
:compileTestJava
:processTestResources
:testClasses
:junitPlatformTest
12 31, 2016 9:39:27 午後 org.junit.platform.launcher.core.ServiceLoaderTestEngineRegistry loadTestEngines
情報: Discovered TestEngines with IDs: [junit-jupiter]
21:39:33.471 [INFO  com.example.ex1.SimpleTest] - 1st test
Test run finished after 11642 ms
[          1 containers found      ]
[         0 containers skipped    ]
[         1 containers started    ]
[         0 containers aborted    ]
[         1 containers successful ]
[         0 containers failed     ]
[         1 tests found           ]
[         0 tests skipped         ]
[         1 tests started         ]
[         0 tests aborted         ]
[         1 tests successful      ]
[         0 tests failed          ]

BUILD SUCCESSFUL

Total time: 12.576 secs

注意点

GradleでJunit5を起動するときに注意したい所は次の点

  • Gradleのバージョンは2.5以上が必要
  • junit-platformjunit-jupiter とがややこしい
    • コンパイル時に必要なライブラリーのgroupが org.junit.jupiter で、gradleのプラグインのほうのgroupが org.junit.platform
    • junit-jupiter のバージョン(いわゆるJUnit5のバージョン)が 5.0.0.-M3junit-platform のバージョン(つまりgradleプラグインのバージョン)が 1.0.0-M3
  • 独自のJUnit5を走らせるタスクを作るのが非常に面倒くさい
    • junitPlatformTest タスクは単なる JavaExec タスクなので、同じようなものを作ろうとするとこれと同じようなコードを書かないといけない
      • まあ、そうではあるんだけど、gradleのissueにJunit5関連のissueが立っているので、gradleから公式のタスクが出るかもしれない
  • デフォルトでクラス名が Test あるいは Tests で終わるものだけをテストクラスとして読み取るようになっている(これとかこれなど)
    • そのため FooSpec というクラスに @Test を付与してもgradleからは起動できない
    • これを回避するためには次のように junitPlatform.filters.includeClassNamePattern あるいは junitPlatform.filters.includeClassNamePatterns に実行したいクラス名のパターンを指定する必要がある
junitPlatform {
  filters {
    includeClassNamePattern '^.*Spec$'
    // あるいは
    includeClassNamePatterns '^.*Spec$', '^.*Tests?$' 
  }
}

DateTimeFormatterのパターンとLocale

Junit5の ParameterResolver によってテストメソッドのパラメーターに渡せる日付文字列の形式をユーザーが自由に決められるようにするために、 DateTimeFormatter を使っていた際に、どうしても月のパターン MMM によって月が名前(例えば1月なら Jan)にならないので、小3時間くらい悩んでました。

結果的には DateTimeFormatter によってフォーマットされる日付の文字列は DateTimeFormatter に渡される Locale も関係するという初歩的だけど、ググってもあまり出てこない(くらいに初歩的な)事実を知らなかったがためにドハマリする結果になりました。

そこで、 LocaleDateTimeFormatter のパターンの組み合わせでどのような日付が出力できるか確認してみました(とはいっても)。

表示内容 パターン ja_JP en_US zh_CN
Y 2017 2017 2017
YY 17 17 17
YYYY 2017 2017 2017
u 2017 2017 2017
uu 17 17 17
uuuu 2017 2017 2017
四半期 q 1 1 1
四半期 qq 01 01 01
四半期 qqq 1 1 1
四半期 Q 1 1 1
四半期 QQ 01 01 01
四半期 QQQ Q1 Q1 1季
四半期 QQQQ 第1四半期 1st quarter 第1季度
M 1 1 1
MM 01 01 01
MMM 1 Jan 一月
MMMM 1月 January 一月
L 1 1 1
LL 01 01 01
LLL 1 1 一月
d 1 1 1
dd 01 01 01
曜日 e 1 1 1
曜日 ee 01 01 01
曜日 eee Sun 星期日
曜日 eeee 日曜日 Sunday 星期日
曜日 c 1 1 1
曜日 ccc Sun 星期日
曜日 cccc 日曜日 Sunday 星期日
曜日 E Sun 星期日
曜日 EE Sun 星期日
曜日 EEE Sun 星期日
曜日 EEEE 日曜日 Sunday 星期日

おわり

Kotlinの末尾再帰でFizzBuzz(Kotlin Advent Calendar 2016) #ktac2016

この記事はKotlin Advent Calendar2016の6日目の記事です。

昨日は RyotaMurohoshiさんの 「【!ってなんだ】KotlinとJava、nullとPlatformType【NullableにNotNull」 でした。

明日は kikuchy さんの 「」 です。


末尾再帰FizzBuzz

この記事ではKotlinの末尾再帰を用いて、ややオーバーエンジニアリングなFizzBuzzのプログラムを書いていきます。 なお、この記事で用いたKotlinのバージョンは1.1-M02-8です。後のアップデートによっては動作しないことがありますが、ご了承ください。

FizzBuzzの型

まず、FizzBuzzの型を定義します。FizzBuzzの抽象的な型を Value とします。次の二点を考慮して Value を定義します。

  • Value は順列なので、次の値を持ちます。
  • Value は最終的に表示するので、文字列への変換を持ちます。

次に具体的な型を考えます。FizzBuzzの値として数値、Fizz/Buzz/FizzBuzz、 また順列の末端要素を用意します。

sealed class Value {
  abstract val show: String
  abstract val next: Value
}

object Term: Value() {
  override val show: String get() = throw UnsupportedOperationException("This is term")
  override val next: Value get() = throw UnsupportedOperationException("This is term")
}
class Num(val value: Int, override val next: Value): Value() {
  override val show: String = "$value "
}
class Fizz(override val next: Value): Value() {
  override val show: String = "Fizz "
}
class Buzz(override val next: Value): Value() {
  override val show: String = "Buzz "
}
class FizzBuzz(override val next: Value): Value() {
  override val show: String = "FizzBuzz\n"
}

FizzBuzz オブジェクト生成

定義した Value を生成していきますが、少しだけ考える必要があります。

単純に1から順番に Valueマッピングしていくと 1 -> 2 -> 3 の順番で Value(n) が作られます。 Value は次へのリンクを持ちますが、これは生成時にすでに存在している Value へのリンクになるため辿っていく順番は Value(3) -> Value(2) -> Value(1) の順番になります。 しかしFizzBuzzでは1から表示していきたいので、これでは不都合です。 Value(n) を作ったときにそのリンク先を一つ後の Value(n + 1) が設定されるようにするために、生成されたばかりの Value を引数にとって先頭の Value(つまり Value(1))を返す関数を作っていきます。また、その関数に Gen という別名を与えます。

typealias Gen = (Value) -> Value

val num: (Int) -> (Gen) -> Gen = { n -> { f -> { v -> f(Num(n, v)) } } }
val fizz:         (Gen) -> Gen =        { f -> { v -> f(Fizz(v)) } }
val buzz:         (Gen) -> Gen =        { f -> { v -> f(Buzz(v)) } }
val fizzBuzz:     (Gen) -> Gen =        { f -> { v -> f(FizzBuzz(v)) } }

なぜこんな面倒くさい関数を作っているかという点に関しては後ほど説明します。

カウント

3の倍数と5の倍数を調べるためのループカウンターを作ります。この型のプロパティには、同じ型で次のカウントを表す next を設定します。 次にこの型に 1 -> 2 -> 3 -> 1 -> ... とループするように最大値をもたせます。 この実装はループの途中を Mid 、ループの終端を End とします。

また、ループカウンターをそのまま末尾再帰関数の呼び出し回数を数えるのにも利用します。末尾再帰の呼び出し回数を数えるものについては Count という 別名を与えます。

interface Succ<L: Succ<L>> {
  val next: L
}

sealed class Loop: Succ<Loop> {
  abstract val max: Int
}

class Mid(val current: Int, override val max: Int): Loop() {
    override val next: Loop get() = if (current == max - 1) End(max) else Mid(current + 1, max)
}
class End(override val max: Int): Loop() {
    override val next: Loop get() = Mid(1, max)
}

typealias Count = Loop

関数用の補助

関数の補助をする関数を作ります。

関数 (P) -> Q と関数 (Q) -> R を合成する拡張関数 plus(P) -> Q につけます。 また、引数をそのまま返す関数 id を作ります。

inline infix operator fun <P, Q, R> ((P) -> Q).plus(crossinline f: (Q) -> R): (P) -> R = { f(this(it)) }

fun <P> id(): (P) -> P = { it }

末尾再帰関数

ではFizzBuzzの列を構築していきます。

tailrec fun run(count: Count, three: Loop = Loop(3), five: Loop = Loop(5), result: Gen = id()): Value =
    when(count) {
      is End -> result(Term)
      is Mid -> when(five) {
        is End -> when(three) {
          is End -> run(count.next, three.next, five.next, fizzBuzz(result))
          is Mid -> run(count.next, three.next, five.next, buzz(result))
        }
        is Mid -> when(three) {
          is End -> run(count.next, three.next, five.next, fizz(result))
          is Mid -> run(count.next, three.next, five.next, num(count.current)(result))
        }
      }
    }

また Value を表示するための末尾再帰関数も作ります。

tailrec fun show(value: Value): Unit = when(value) {
  is Term -> Unit
  else    -> show(value.next.apply { print(value.show) })
}

生成した値をそのまま返すのではなく、関数にくるんで再び関数を適用していくやりかたを継続渡し(CPS)というらしいのですが、これを用いたところにKotlinで末尾再帰を書くときのコツがあります。関数ではなく、オブジェクトの順番に気をつけて Value をそのまま返すやり方を使ったらおそらくこのようになるでしょう。

tailrec fun run(count: Count, three: Loop = Loop(3), five: Loop = Loop(5)): Value =
// 中略
// Num だった場合
  is Mid -> Num(count.current, run(count.next, three.next, five.next))

しかし、このように定義した場合、関数 run は残念ながら末尾再帰になりません。

関数を末尾再帰にする場合、その関数は関数内部で一番最後に呼び出される関数にする必要があります。上記の場合であれば run 関数が呼ばれた後に Num コンストラクターが呼ばれており、再帰呼出しではあっても、末尾呼び出しではありません。このような場合、Kotlinコンパイラーは run を呼び出している箇所に「Recursive call is not a tail call」、 run 関数に「A function is marked tail-recursive but no tails call are found」と警告を出します。

そこでオブジェクトのリンクを作る処理を関数に閉じ込めて、再帰処理が終わった後に改めてその処理を実行するようにするため関数を作っていくようにしたわけです。

呼び出し

最終的に最大値(Int)からFizzBuzzの列(Value)を構築して、それを表示する関数(show)を呼び出して終わる(Unit)関数を作ります。

fun from1(max: Int): Count = Mid(1, max + 1)
val runApp: (Int) -> Unit = ::from1 + ::run + ::show

fun main(args: Array<String>): Unit = runApp(30)

Kotlinの末尾再帰によるFizzBuzzでした。末尾再帰は結果となる値を

  • 継続渡しスタイル(今回の関数)
  • 循環するデータにする(今回の Value のような構造)

ことがポイントです。

おわり

Docker復習

歳なので覚えるのが遅くなったというよりも忘れるのが早くなった感じらしく、Dockerのことをすっかり忘れてたので、復習した。Dockerの使い方レベルなので自分が得するだけのエントリー。

dockerコンテナに環境変数を渡す

-e VARIABLE=VALUE で渡す。

docker run -it --rm mysql \
  -e MYSQL_DATABASE=sample \
  -e MYSQL_USER=user \
  -e MYSQL_PASSWORD=password \
  -e MYSQL_RANDOM_ROOT_PASSWORD=yes

dockerのポート番号の指定

-p ローカルのポート:コンテナ内部から見たポート で渡す

dockerのディレクトリのマウント

ローカルにあるディレクトリをコンテナにマウントすることができる

-v ローカルのディレクトリ/コンテナのディレクトリ

mysqlイメージ起動時に指定する変数

ポート番号

  • mysqlのイメージはポート3306をexposeしている

環境変数

名前 意味
MYSQL_DATABASE データベース名(USER/PASSWORDを指定する必要がある)
MYSQL_USER データベースのユーザー名
MYSQL_PASSWORD 上記ユーザーのパスワード
MYSQL_RANDOM_ROOT_PASSWORD ルートパスワードをランダムにするか(yes:する,no:しない)

データディレクトリ等

最近起動したDockerコンテナをkillする

docker killdocker ps -q を組み合わせる

docker kill `docker ps -q`