【サーバーサイドKotlin】ことりん × テスト with SpringFramework

はじめに

なぜサーバーサイドKotlinを導入するのか?であるように、現在アプリボットでは、新規開発プロダクトのサーバーサイド言語でKotlin導入を進めています。
そこで、テストコードについてもKotlin化しましたのでご紹介します。

なお、この記事は2018年1月時点の情報なので今はもっと良い方法があるかもしれません。

 

 

以前のJavaテストコード

以前はJavaで以下のようなテストコードを書いていました。
テスト時の条件や期待値がわかりやすいように日本語でメソッド名を書いています。

HelloWorldJavaTest.java
public class HelloWorldJavaTest {
   @Autowired // <-------------------------------------- Spring Beanの注入
   private HelloWorldService testClass;
   @Before // <----------------------------------------- JUnit テスト事前処理
   public void setUp() {
       // 前提条件を整える(テストデータ準備、日時の固定化等)
   }
   @Test // <------------------------------------------- JUnit テスト
   public void method1_口口の場合_口口になる() {
       new MockUp<ChildService>(ChildService.class) {
           @Mock // <----------------------------------- JMockit 外部依存メソッドのモック化
           public int childMethod1() {
               return 2;
           }
       };
       Assertions.assertThat(testClass.method1()) // <-- AsserJ アサーション(検証処理)
           .as("処理結果が期待通りであることを確認")
           .isEqualTo(2);
   }
}

使用しているフレームワークは以下のものです。

分類 名前
テストフレームワーク JUnit4
モックフレームワーク JMockit
検証フレームワーク AssertJ

Kotlinでのテストコード

KotlinではJavaのテストフレームワークもほぼ使えますが、Kotlinに特化したテストフレームワークがあります。

Kotlinの代表的なテストフレームワーク

分類 名前
テストフレームワーク Spek
KotlinTest
モックフレームワーク Mockit-Kotlin
検証フレームワーク Kluent
Expekt
Hamkrest

これらのうち、今回はテストフレームワークとモックフレームワークを試してみます。

試したこと その1: Spek (テストフレームワーク)

BDD(振る舞い駆動開発)フレームワークでもあるため、振る舞い(仕様)を記載しやすい構造になっています。
以前のJavaテストコードよりこちらの方がメンテナンスもしやすいと考え、検証をすすめました。

HelloWorldKotlinSpecTest.kt
object HelloWorldKotlinSpecTest : Spek({

   given("HelloWorldServiceクラス") {
       val testClass = HelloWorldService()

       on("method1メソッドが□□の場合") {
           val result = testClass.method1()

           it("□□になる") {
               Assert.assertEquals(2, result)
           }
       }
   }
})

検証すすめていたところ・・・

Spring Beanの注入が出来ない!!

テストコードを動かす際にSpringFrameworkで管理しているBeanの注入が出来ませんでした。
SprintFrameworkのテストサポートでは、@Testアノテーションがついたテストメソッドが必須のようです。
ところが、Spekだとコンストラクタ引数にテストコードを記載するため、テストメソッドが定義できません。

試したこと その2: KotlinTest (テストフレームワーク)

StringSpec, FunSpec, ShouldSpec, WordSpec, FeatureSpec, BehaviorSpec等、様々な書き方がサポートされており多機能なフレームワークです。
ここでは、上記のSpekと同等な記載方法である、BehaviorSpecを試してみました。

HelloWorldKotlinTest.kt
// StringSpec, FunSpec, ShouldSpec, WordSpec, FeatureSpec, BehaviorSpec(以下の例で使用), FreeSpec がある。
// PropertyTesting, TableDrivenTesting等の仕組みも用意されている。
object HelloWorldKotlinTest : BehaviorSpec() {
   init {
       given("HelloWorldServiceクラス") {
           val testClass = HelloWorldService()

           `when`("□□の場合") {
               val result = testClass.method1()

               then("□□になる") {
                   Assert.assertEquals(2, result)
               }
           }
       }
   }
}

こちらもSpekと同じく・・・

やっぱり、Spring Beanの注入が出来ない!!

試したこと その3: Mockit-Kotlin (モックフレームワーク)

このモックフレームワークは、Javaでも有名なMockitを、Kotlinから扱いやすく関数でラップしたフレームワークです。
以下の通りChildService.childMethod1()の部分モック化に成功しました。

HelloWorldKotlinTest.kt
class HelloWorldKotlinTest {

   @Autowired
   private lateinit var testClass: HelloWorldService

   @Test
   fun method1_口口の場合_口口になる() {
       val mock = mock<ChildService> {
           on { childMethod1() } doReturn 2 // <--- mockit-kotlin 外部依存メソッドの部分モック化
       }

       Assertions.assertThat(testClass.method1())
           .`as`("処理結果が期待通りであることを確認")
           .isEqualTo(2)
   }
}

しかし、テストメソッドを増やしていったら問題が起きました・・・

あるテストメソッド内で部分モック化したクラスChildServiceが、後続のテストメソッドで正常に動作しない場合がありました。ChildServiceが自分自身のフィールドを参照する処理が実行される際に、フィールドが常にnullに見えてしまうのです。
調査を進めてみると、部分モック化した外部クラスがCGLIB Proxy(SpringAOPによる処理のインジェクション時に利用されるProxyクラス) でラップされている場合に起こる事象のようです。
Proxyが正常に動作せずにChildServiceの実態クラスが持つフィールドを見ることが出来ていないような挙動でした。

結果、Kotlinで採用したテストコードの書き方

以下のようなテストコードで書く方針にしました。
以前のJavaテストコードとそっくりですね。

HelloWorkdJavaTest.kt
class HelloWorldJavaTest {
   @Autowired // <------------------------------------------------- Spring Beanの注入
   private lateinit var testClass: HelloWorldService

   @Before // <---------------------------------------------------- JUnit テスト事前処理
   fun setUp() {
       // 前提条件を整える(テストデータ準備、日時の固定化等)
   }

   @Test // <------------------------------------------------------ JUnit テスト
   fun method1_口口の場合_口口になる() {
       object : MockUp<ChildService>() {
           @Mock // <---------------------------------------------- JMockit 外部依存メソッドのモック化
           fun childMethod1(): Int {
               return 2
           }
       }
       Assertions.assertThat(testClass.method1()) // <------------- AsserJ アサーション(検証処理)
               .`as`("処理結果が期待通りであることを確認")
               .isEqualTo(2)
   }
}

Kotlin+SpringFrameworkでのテストコードを書く際に問題が発生したこともありますが、
従来のJava技術者の移行しやすさも考え、まずはJavaと似た感じのテストコードとしました。
Kotlinの代表的な検証フレームワーク(Kluent, Expekt, Hamkrest)は今回試しませんでしたが、今後Kotlinでのテストコードでノウハウを蓄積しながら段階的に試してきたいと思います。

まとめ

Kotlinのテストフレームワークを使用する際、SpringFrameworkとの相性問題が出ることがありました。
しかし、KotlinはJavaとの親和性が高いため、Javaのテストフレームワークを使用することでもテストコードを記述することができました。

Kotlinを正式にサポートしているSpring5が出てまだ日が浅いため、上記のような相性問題は残っていると思いますが、良いテストコードが書けるように引き続き検証を進めて行きたいと思います。