
【サーバーサイドKotlin】KotlinTestによるKotlin × SpringBootの単体テスト
こんにちは、サーバーサイドエンジニアの竹端です。
以前【サーバーサイドKotlin】ことりん × テスト with SpringFrameworkにてKotlinのテストフレームワークの検証内容をご紹介をしました。
この時はSpek、KotelinTestというKotlin製のフレームワークがSpring Frameworkとの相性の問題で使えず、JUnitを採用するという検証結果になっていました。
しかし、その後KotlinTestにアップデートがあり、Spring FrameworkのDIに対応がされたため、そちらへ移行することとなりました。
今回はKotlinTestをSpring Frameworkを併せて使った際のテストコードの書き方についてご紹介します。
今回使用する環境
- Kotlin 1.2.51
- KotlinTest 3.1.8
- SpringBoot 2.0.4.RELEASE
事前準備
テスト対象のクラスの用意
今回テストに関する内容のご紹介ということで、先にテストを実行する対象のクラスを作っておきます。
class SampleService {
fun execute(param: Int): String {
if (param == 1) {
return "one"
}
if (param == 2) {
return "two"
}
return "default"
}
}
パラメータに1を渡したらone、2を渡したらtwo、それ以外ならdefaultという文字列を返すだけの関数を持ったクラスです。
テストの紹介が目的なので、こちらはシンプルなものになっています。
Gradleに依存関係の追加
Gradleのdependenciesに、下記を追加します。
testCompile("io.kotlintest:kotlintest-runner-junit5:3.1.8")
testCompile("io.kotlintest:kotlintest-extensions-spring:3.1.8")
Springを使用しない場合は、1行目の設定のみで大丈夫です。
KotlinTestとは?
KotlinTestはKotlin製のテストフレームワークで、多機能であることが特徴です。
StringSpec, FunSpec, ShouldSpec, WordSpec, FeatureSpec, BehaviorSpec、AnnotationSpec等、様々な書き方がサポートされています。
まずは、この中の2つの書き方をご紹介します。
書き方の例の紹介
BehaviorSpec
以前の記事で検証に使っていた記述方法です。
Behaviorという言葉の通り、振る舞い駆動開発をするのにも適した書き方になっています。
class SampleServiceForBehaviorSpecTest : BehaviorSpec() {
init {
val service = SampleService()
given("executeで") {
`when`("paramが1の場合") {
val result = service.execute(1)
then("oneが返る") {
result shouldBe "one"
}
}
`when`("paramが2の場合") {
val result = service.execute(2)
then("twoが返る") {
result shouldBe "two"
}
}
}
}
}
BehaviorSpec
という抽象クラスを継承することで、BehaviorSpecの書き方をすることができます。
他の〜Specという書き方も、同じように継承するクラスを変えることで、その書き方をできるようになります。
また、実行結果の検証処理にもKotlinTestの機能を使っています。
このサンプルコードでは shouldBe
をというキーワードを使うことで、値の等価性を検証しています。
BehaviorSpecに関しては、 given
when
then
という3つのブロックで構成され、分かりやすいコードになることと、実装者による差分が生まれづらいことを魅力に感じ、こちらを試していました。
実行結果も次のように、階層構造で表示されます。
StringSpec
現在アプリボットで採用している記述方法がこちらになります。
最もシンプルな記述方法で、下記のようなコードになります。
class SampleServiceForStringSpecTest : StringSpec() {
init {
val service = SampleService()
"executeでparamが1の場合oneが返る" {
service.execute(1) shouldBe "one"
}
"executeでparamが2の場合twoが返る" {
service.execute(2) shouldBe "two"
}
}
}
文字列でテストケースの名前を定義し、その中で検証処理を書く非常にシンプルな形式になります。
実行結果は次のようになります。
StringSpecを採用した理由
StringSpecはKotlinTestの開発者も推奨しており、公式ドキュメントでの情報も充実しています。
また、KotlinTestには特定のテストのみの実行、除外をするためにFocus、Bangという機能が用意されているのですが、こちらがsingle top level testのみをサポートしているため、BehaviorSpec等では正常に動かない場合がありました。
このことも、StringSpecの採用に傾いた要因の一つになりました。
実装例は下記になります。
Focus
名前の先頭に f:
を付けたテストケースのみ実行されます。
val service = SampleService()
"executeでparamが1の場合oneが返る" {
service.execute(1) shouldBe "one"
}
"f:executeでparamが2の場合twoが返る" {
service.execute(2) shouldBe "two"
}
実行結果
Bang
名前の先頭に !
を付けたテストケースを除外して実行されます。
val service = SampleService()
"executeでparamが1の場合oneが返る" {
service.execute(1) shouldBe "one"
}
"!executeでparamが2の場合twoが返る" {
service.execute(2) shouldBe "two"
}
実行結果
forallを使ったデータ駆動テスト
KotlinTestの強力な機能の一つとして、forallによるデータ駆動テストがあります。
下記のようなテストの書き方ができます。
init {
val service = SampleService()
forall(
row(1, "one"),
row(2, "two")
) { param, result ->
"executeでparamが${param}の場合${result}が返る" {
service.execute(param) shouldBe result
}
}
}
row
に書いてある値をパラメータをparam、resultという名前で受け取り、それぞれのパラメータで下のテストコードを実行します。
このコードでは、先程のexecuteという関数に1、2の2つの値を渡して実行し、それぞれ併せて設定しているone、twoという値が返ってきているかをテストしています。
こうすることによって、先程のStringSpecのテストコードを一つにまとめることができました。
このように、異なるパラメータの組み合わせで実行していくテストのことを、データ駆動テストと言います。
実行結果も先程と同じように表示されます。
また、下記のような関数のテストをしようとした場合も便利です。
fun execute2(param: Int): Boolean {
if (param >= 10 && param <= 100) {
return true
} else {
return false
}
}
通常であれば境界値のテストとして、
- paramが10の場合true
- paramが100の場合true
- paramが9の場合false
- paramが101の場合false
という4パターンのテストコードを書く必要があります。
しかし、KotlinTestのforallを使用すれば
forall(
row(10, "最小値"),
row(100, "最大値")
) { num, description ->
"execute2でparamが${description}の場合trueが返る" {
val service = SampleService()
service.execute2(num) shouldBe true
}
}
forall(
row(9, "最小値-1"),
row(101, "最大値+1")
) { num, desctiption ->
"execute2でparamが${desctiption}の場合falseが返る" {
val service = SampleService()
service.execute2(num) shouldBe false
}
}
このように境界値の上下のテストを一つにして書けるので、余分なテストデータやテストコードを削減することができます。
他にもランダムな値を渡して繰り返し実行してくれる機能など、非常に魅力的な機能もあるので、詳しくは公式ドキュメントを読んでいただければと思います。
Spring Frameworkを使った場合のテスト
さて、ここまででKotlinTestについては説明してきましたが、いよいよSpring Frameworkと合わせたテストについて書いていこうかと思います。
※SpringBootを使用するためのGradleの設定は今回は省略しますので、Spring Initializr等を使用して環境に併せて作成していただければと思います。
まず、最初に記載したテスト対象のクラスをSpringの推奨の形に併せてインターフェース、実装クラスに分けます。
interface SpringSampleService {
fun execute(param: Int): String
}
@Component
class SpringSampleServiceImpl : SpringSampleService {
override fun execute(param: Int): String {
if (param == 1) {
return "one"
}
if (param == 2) {
return "two"
}
return "default"
}
}
実装クラスの方にはDIの対象とするための @Component
を忘れずに付けてください。
次に、Springの設定クラスを作ります。
@ComponentScan
class TestApplicationContext
今回はComponentScanで全パッケージを対象とする設定だけをアノテーションでしています。
次がテストクラスです。
@ContextConfiguration(classes = [TestApplicationContext::class])
class SpringSampleServiceImplTest(
private val service: SpringSampleService
) : StringSpec() {
init {
"executeでparamが1の場合oneが返る" {
service.execute(1) shouldBe "one"
}
"executeでparamが2の場合twoが返る" {
service.execute(2) shouldBe "two"
}
}
}
@ContextConfiguration
のアノテーションでパラメータに先程作った設定クラスを渡します。
また、今回はSpringで最も一般的なConstructor Injectionで SpringSampleService
のDIをしています。
そして、次がKotlinTestでSpringを扱う際の重要なポイントとなります。
AbstractProjectConfig
というクラスを継承し、下記のクラスを実装します。
package io.kotlintest.provided
import io.kotlintest.AbstractProjectConfig
import io.kotlintest.extensions.ProjectLevelExtension
import io.kotlintest.spring.SpringAutowireConstructorExtension
class ProjectConfig : AbstractProjectConfig() {
override fun extensions(): List<ProjectLevelExtension> = listOf(SpringAutowireConstructorExtension)
}
ここでオーバーライドしている extensions
メソッドで SpringAutowireConstructorExtension
というクラスを設定して返すことで、SpringのConstructor Injectionがテストで使えるようになります。
この時注意が必要で、パッケージは io.kotlintest.provided
に配置し、クラス名は ProjectConfig
という名前を必ず固定で付けなくてはいけません。
そのため、このサンプルコードのみパッケージ定義含めた全行を記載しています。
この extensions
メソッドで返しているListに追加して返すことで、様々な機能が使えるようになります。
また、AbstractProjectConfigの中には listeners
filters
という関数も用意されており、併せて設定が必要になる場合があります。
今回はConstructor Injectionを使う設定のみを追加しているので、フィールドインジェクションを使う場合も別の設定が必要になります。
そちらも詳しくは公式ドキュメントをご覧いただければと思います。
最後に
今回はKotlinTestのご紹介と、Spring Frameworkで使用する際の方法をご紹介しました。
Spring対応が入ってからまだ間もないこともあり、日本語での情報も少ないので、こちらの記事がこれから使う方のお役に少しでも立てばと思います。
また、アプリボットではここから更にプロダクトで扱いやすいよう共通化し、テスト基盤を作っています。
そちらも、いずれこのブログでご紹介できればと思います。
この記事へのコメントはありません。