【サーバーサイド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

事前準備

テスト対象のクラスの用意

今回テストに関する内容のご紹介ということで、先にテストを実行する対象のクラスを作っておきます。

SampleService.kt
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という言葉の通り、振る舞い駆動開発をするのにも適した書き方になっています。

SampleServiceForBehaviorSpecTest.kt
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つのブロックで構成され、分かりやすいコードになることと、実装者による差分が生まれづらいことを魅力に感じ、こちらを試していました。

実行結果も次のように、階層構造で表示されます。

4c48ee2c-1310-7758-1302-ed53501712c7

StringSpec

現在アプリボットで採用している記述方法がこちらになります。

最もシンプルな記述方法で、下記のようなコードになります。

SampleServiceForStringSpecTest.kt
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"
        }
    }
}

文字列でテストケースの名前を定義し、その中で検証処理を書く非常にシンプルな形式になります。

実行結果は次のようになります。

 

ddc2c357-1ca1-472a-a21d-e21f0768bf0e

StringSpecを採用した理由

StringSpecはKotlinTestの開発者も推奨しており、公式ドキュメントでの情報も充実しています。

また、KotlinTestには特定のテストのみの実行、除外をするためにFocusBangという機能が用意されているのですが、こちらが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"
}

実行結果

215ed813-1bb2-86d7-77bd-cd079c6aa483

Bang

名前の先頭に ! を付けたテストケースを除外して実行されます。

val service = SampleService()

"executeでparamが1の場合oneが返る" {
    service.execute(1) shouldBe "one"
}

"!executeでparamが2の場合twoが返る" {
    service.execute(2) shouldBe "two"
}

実行結果

8907d577-d5b8-a2f0-a618-46d6d8e7955b.png

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のテストコードを一つにまとめることができました。

このように、異なるパラメータの組み合わせで実行していくテストのことを、データ駆動テストと言います。

実行結果も先程と同じように表示されます。

3e37242e-ceba-9e84-17fb-ebfecdb76879.png

 

また、下記のような関数のテストをしようとした場合も便利です。

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の推奨の形に併せてインターフェース、実装クラスに分けます。

SpringSampleService.kt
interface SpringSampleService {
    fun execute(param: Int): String
}
SpringSampleServiceImpl.kt
@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の設定クラスを作ります。

TestApplicationContext.kt
@ComponentScan
class TestApplicationContext

今回はComponentScanで全パッケージを対象とする設定だけをアノテーションでしています。

次がテストクラスです。

SpringSampleServiceImplTest.kt
@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 というクラスを継承し、下記のクラスを実装します。

ProjectConfig.kt
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対応が入ってからまだ間もないこともあり、日本語での情報も少ないので、こちらの記事がこれから使う方のお役に少しでも立てばと思います。

また、アプリボットではここから更にプロダクトで扱いやすいよう共通化し、テスト基盤を作っています。

そちらも、いずれこのブログでご紹介できればと思います。

 

Unity2018.1からのBuildPlayerを試す

Unity 2018.1より、BuildPipeline.BuildPlayerのAPIが更新されました。

この記事ではその変更内容と、実際に導入してみて少しハマった事例について紹介したいと思います。

APIの変更点

Unity 2017.xまでは、BuildPipeline.BuildPlayerの戻り値はstringでした。

この戻り値はそのビルドでエラーが発生した際に、そのエラー内容を 文字列 で返却します。

Unity2018.1からはこの戻り値がstringではなく、Build.Reporting.BuildReportを返却するようになりました。

Build.Reporting.BuildReportとは

Build.Reporting.BuildReportは文字通り、Unityのビルドプロセスの結果を保持するクラスです。

結果クラスを返してくれることで、ビルド時にどういう過程がありどの程度時間がかかったのか、どういう箇所でエラーが発生したのかなど、ビルドの詳細を把握しやすくなりました。

このクラスの保持するデータについて、説明していきます。

BuildReport.summary

文字通り、ビルド結果の概要を保持するクラスになります。 よく使いそうな変数を列挙します。

変数名 説明
result ビルド結果をBuildResultで保持します
totalErrors ビルドの総エラー数を保持します
totalWarnings ビルドの総警告数を保持します
totalSize ビルド成果物のサイズをbytesで保持します
totalTime ビルドの総時間をSystem.TimeSpanで保持します

ビルドそのものが成功したかどうかは、result変数を介して取得することができます。

後述しますが、ビルドが失敗したときには、いままでは例外を投げていたのですが、2018.1からは例外を投げなくなったので、result変数をみてエラーハンドリングをする必要があります。

また、警告やエラー数・ビルド成果物のサイズを取得できるようになったため、Jenkinsなどでビルドしたときのビルドの統計情報を作成が楽になりました。

BuildReport.steps

ビルドの各ステップの情報を配列で保持しています。

変数名 説明
name ビルドステップの名前を保持します
depth ビルドステップのネストの深さを保持します
duration そのステップに要した時間を秒単位で保持します
messages そのステップでのログの一覧を保持します。各メッセージは、ログの種類(Warning, Errorなど)と実際のメッセージを保持します

namedurationによって、各ビルドにはどういう工程が存在し、それらがどの程度時間がかかっているかがわかるようになったため、

例えば、ビルド時間の改善の手がかりなどにうまく利用できるのではないのかなと思います。

また、各ビルドステップごとにログを吐いてくれるのも嬉しところです。

ちなみに実験的ではありますが、ビルドステップに Compile scriptsという工程がありますが、プロジェクトで記述したスクリプトの警告ログもここに吐くようです。

こちらのログを収集して、例えばJenkinsでCIをしているなら通知することで、プロジェクトの品質維持に効果的かもしれません。

BuildReport.files

ビルドプロセスで生成されたファイル情報が配列で保持されています。

変数名 説明
path 生成されたファイルの絶対パスを保持します
role 生成されたファイルの役割を保持します
size ファイルサイズをbyte形式で保持します

実験的ではありますが、具体的に、空のサンプルプロジェクトのiOSビルドした際には、以下のような情報が取得できました。

BuildFile path: /path-to-project/Temp/StagingArea/Data/Managed/UnityEngine.UI.dll
BuildFile role: ManagedLibrary
BuildFile size: 250368
BuildFile path: /path-to-project/Temp/StagingArea/Data/Managed/UnityEngine.Networking.dll
BuildFile role: ManagedLibrary
BuildFile size: 255488
BuildFile path: /path-to-project/Temp/StagingArea/Data/Managed/UnityEngine.Timeline.dll
BuildFile role: ManagedLibrary
BuildFile size: 92672
BuildFile path: /path-to-project/Temp/StagingArea/Data/Managed/UnityEngine.SpatialTracking.dll
BuildFile role: ManagedLibrary
BuildFile size: 9216
BuildFile path: /path-to-project/Temp/StagingArea/Data/globalgamemanagers
BuildFile role: GlobalGameManagers
BuildFile size: 28776
BuildFile path: /path-to-project/Temp/StagingArea/Data/level0
BuildFile role: Scene
BuildFile size: 5281
BuildFile path: /path-to-project/Temp/StagingArea/Data/globalgamemanagers.assets
BuildFile role: SharedAssets
BuildFile size: 24908
BuildFile path: /path-to-project/Temp/StagingArea/Data/sharedassets0.assets
BuildFile role: SharedAssets
BuildFile size: 21160
BuildFile path: /path-to-project/Temp/StagingArea/Data/Resources/unity_builtin_extra
BuildFile role: BuiltInShaders
BuildFile size: 638968
BuildFile path: /path-to-project/Temp/StagingArea/Data/boot.config
BuildFile role: BootConfig
BuildFile size: 55
BuildFile path: /path-to-project/Temp/StagingArea/Data/unity default resources
BuildFile role: Unity default resources
BuildFile size: 6133660
BuildFile path: /path-to-project/Temp/StagingArea/Data/Managed/mono/2.0/machine.config
BuildFile role: Mono 2.0 machine.config
BuildFile size: 27625
BuildFile path: /path-to-project/Temp/StagingArea/Data/Managed/mono/4.0/machine.config
BuildFile role: Mono 4.0 machine.config
BuildFile size: 33648
BuildFile path: /path-to-project/iOSBuild/Libraries/libiPhone-lib.a
BuildFile role: Unity Player static library
BuildFile size: 539456888
BuildFile path: /path-to-project/iOSBuild/Libraries/libil2cpp.a
BuildFile role: IL2CPP static library
BuildFile size: 97469512

BuildReport.strippingInfo

このビルドに含まれているUnityEngineのネイティブコードモジュールの一覧と、その理由に関する情報を保持します。

この変数は、そのビルド対象のプラットフォームがcode strippingをサポートするプラットフォームであるときのみ利用できます。

サポートしない場合は、nullが代入されます。

含まれているネイティブコードモジュールの一覧を取得するためには、BuildReport.strippingInfo.includedModulesプロパティを参照します。

そのモジュールがなぜ含まれているのかを知るには、BuildReport.strippingInfo.GetReasonsForIncludingに、モジュール名を引数で渡すことで取得できます。

エラーハンドリングの変更点

いままでBuildPipeline.BuildPlayerは、ビルド中にエラーが発生すると、例外を投げる実装になっていましたが、

Unity 2018.1からは、ビルド成功有無にかかわらず、例外を投げないようになりました。

Unityをバッチモードで起動した場合の戻り値は、例外を投げた場合には1EditorApplication.Exitを実行した場合は、

そのExitの引数がそのまま戻り値に、それ以外の場合は0、つまり成功という扱いになります(参考: -executeMethod)

そのため例えば下記のように、単純にBuildPipeline.BuildPlayerを実行するだけでは、ビルドが失敗した場合でも成功扱いになってしまいます。

using UnityEditor;
using UnityEngine;

// 引用: https://docs.unity3d.com/2018.1/Documentation/ScriptReference/BuildPipeline.BuildPlayer.html
public class BuildPlayerExample : MonoBehaviour
{
    public static void MyBuild()
    {
        BuildPlayerOptions buildPlayerOptions = new BuildPlayerOptions();
        buildPlayerOptions.scenes = new[] { "Assets/Scene1.unity", "Assets/Scene2.unity" };
        buildPlayerOptions.locationPathName = "iOSBuild";
        buildPlayerOptions.target = BuildTarget.iOS;
        buildPlayerOptions.options = BuildOptions.None;
        BuildPipeline.BuildPlayer(buildPlayerOptions);
    }
}
# 下記のコマンドが、戻り値0となる
# 例えばJenkinsビルドで、戻り値が非0で失敗するように組んでいると
# 失敗していようが成功扱いとしてしまう
/Path/To/Unity -batchmode -executeMethod BuildPlayerExample.MyBuild -buildTarget iOS

一方今回の変更で、ビルド情報がBuild.Reporting.BuildReportに含まれるようになりました。

適切にエラーハンドリングを行うためには、BuildReport.summary.resultを見てやる必要があります。

下記に実装例を記載します。

#if UNITY_EDITOR
using UnityEditor;
using UnityEditor.Build.Reporting;
using UnityEngine;
using System.Text;

// 引用: https://docs.unity3d.com/2018.1/Documentation/ScriptReference/BuildPipeline.BuildPlayer.html
public class Builder : MonoBehaviour
{
    public static void MyBuild()
    {
        BuildPlayerOptions buildPlayerOptions = new BuildPlayerOptions();
        buildPlayerOptions.scenes = new[] { "Assets/Scenes/Scene1.unity" };
        buildPlayerOptions.locationPathName = "iOSBuild";
        buildPlayerOptions.target = BuildTarget.iOS;
        buildPlayerOptions.options = BuildOptions.None;

        BuildReport buildReport = BuildPipeline.BuildPlayer(buildPlayerOptions);

        // // buildReport.resultを見て、戻り値を決定する
        if (buildReport.summary.result == BuildResult.Succeeded)
        {
            const int kSuccessCode = 0;
            EditorApplication.Exit(kSuccessCode);
        }
        else
        {
            const int kErrorCode = 1;
            EditorApplication.Exit(kErrorCode);
        }
    }
}
#endif

まとめ

Unity 2018.1におけるBuildPipeline.BuildPlayerについての変更点と、変更に合わせてハマった点について紹介しました。

もともとのBuildPipeline.BuildPlayerは返す情報がstringだけだったので、おそらく多くの開発者がEditor.logなどを解析して、CIでの情報を充実させることが多かったと思うので、この変更は良いなと感じました。

 

CEDEC 2018セッション内容のご紹介「ゲーム開発に最適なサーバーサイドKotlin 〜Kotlinの導入と基盤ができるまで〜」

こんにちは、サーバーエンジニアの竹端です。

この度、2018年8月22日(水)〜24日(金)で行われるCEDEC 2018に、登壇させていただくことになりました。

そこで、今回はみなさんにそのセッションへ興味を持っていただくべく、内容やポイントを簡単にご紹介したいと思います。

続きを読む “CEDEC 2018セッション内容のご紹介「ゲーム開発に最適なサーバーサイドKotlin 〜Kotlinの導入と基盤ができるまで〜」”

Unityアプリで簡単ソケット通信

アプリボット ゲームプログラマの220Rnです。

ゲームを開発していて、例えばステージデータなどを頻繁に更新し、

アプリに反映させてテストプレイをしたい!などと思ったことはありませんか。

ありますよね。そのようなとき、サーバを立てるまでもないものの、

アプリとPCで通信してデータのやり取りをすることで作業の効率化を図りたいと思いました。

そこで、ソケット通信で自作ツールとUnityアプリを接続して

簡単なデータ通信を実装した方法について紹介します。

続きを読む “Unityアプリで簡単ソケット通信”

ソーシャルゲームの価値を上げるログデータのつくりかた

はじめに

現在データ分析基盤の再構築を担当している、サーバーサイドエンジニアの小川詩織です。これまで私は4つのソーシャルゲームの新規開発・運用を経験してきました。そこでの知見と考察をまとめます。

ログデータは、調査などに必要なただの履歴という立場に置かれがちです。ですが、作業工数を大幅カットしたり、定量的な効果測定や判断ができるなど、適切なログ設計と活用により利益に繋がる施策の指針にすることも出来るものです。

ログには、コンテンツ内容により色々な種類や設計があるため、全てに共通する最適解はありません。ですが設計の指針となるべき事柄はあるので、ログの種類や活用例、設計の仕方、工夫などについて入門的な内容について一通り触れていきます。その中でログの可能性も一緒に感じていただきたいと考えています。

続きを読む “ソーシャルゲームの価値を上げるログデータのつくりかた”

UnityIAP subscription 対応時の備忘メモ

はじめに

リトルチャンピオンズ では、定期購読型(subscription)課金実装にUnityIAPを使用しました。

今回はその実装を行った際の知見を紹介したいと思います。

subscription以外の実装例については前回の記事 を参照ください。

また、サーバー側の実装についてはこちらの記事 を参照ください。

続きを読む “UnityIAP subscription 対応時の備忘メモ”

ゲームにおける課金システムについて(自動更新購読編)

こんにちは、アプリボット サーバーサイドエンジニアの森です。

iOS/Android用3Dアクションゲーム『リトルチャンピオンズ』をリリースするにあたり、自動更新購読型の課金を導入しました。

今回は、アプリケーションサーバーでの課金処理と自動更新購読型について紹介します。

続きを読む “ゲームにおける課金システムについて(自動更新購読編)”