- 投稿日:2019-02-03T21:52:32+09:00
Ktorでテスト入門 〜とりあえずgetとpostをやってみる〜
概要
サーバーサイドKotlinフレームワークのKtorのスモークテストやるぞ!というわけで今回は、以下のドキュメントからgetとpostのテスティングを抽出して記事化しました!
Testing Server Applications環境
- macOS High Sierra 10.13.6
- IntelliJ IDEA 2018.3.1
- Java 1.8.0_191
- Kotlin 1.3.11
- Ktor 1.1.1
Ktorの環境構築とかはこちらが参考になるかと。
KtorでHello World構成
今回はDocker Composeで構築したnginxにKtorを導入してます(割愛)
コード
まず、共通処理としてテスト用のControllerを作成します。
doWithTestApplicationメソッドは設定読み込みとバグ回収のためwith(engine)を読み出しています。
この設定を読み込む際にDB周りも初期化されるため、DB Connection数の上限に気をつけてください。
getメソッドとpostメソッドはそれぞれのエンドポイントに対して、それぞれのメソッドで呼び出すためのものです。doWithTestApplicationでラップして、そのコールバックに処理を書いています。
基本的にはRequest Headerの設定とpostの場合のみ、bodyの指定を行っています。APITestController.kt@KtorExperimentalAPI class APITestController { private fun doWithTestApplication(proc: TestApplicationEngine.() -> Unit) { val engine = TestApplicationEngine(createTestEnvironment { config = ///設定を読み込む処理 }) engine.start(wait = false) /// https://github.com/ktorio/ktor/issues/358#issuecomment-372897523 with(engine) { proc() } } fun get(url: String, headers: Headers? = null, proc: TestApplicationCall.() -> Unit) = doWithTestApplication { handleRequest(HttpMethod.Get, url) { headers?.forEach { s, list -> list.forEach { addHeader(s, it) } } }.proc() } fun post(url: String, body: String, headers: Headers? = null, proc: TestApplicationCall.() -> Unit ) = doWithTestApplication { handleRequest(HttpMethod.Post, url) { headers?.forEach { s, list -> list.forEach { addHeader(s, it) } } setBody(body) }.proc() } }テストケース側では、上記のControllerを使ってテストケースを組み立てます。
getの場合、header指定がないのであればctrl.get("エンドポイント")みたいな書式でかけます。
コールバックに正常系期待値判定を行います。以下の/xxx/aaa の場合、httpレスポンスが200なら正、としています。
postの場合、ボディとヘッダーの指定が必要となります。(最初ヘッダーを指定していなくてエラーでハマった…)
ボディはlistOf("key" to "value").fromUrlEncode()とすることで比較的平易に値の設定が可能になっています。
ヘッダーはHeaders.buildのコールバックにappendで HttpHeadersのプロパティ値をキーに、値は例にあるように、たとえばContentTypeであればContentType.Application.FormUrlEncoded.toString()のような形で指定可能です。最初、content typeを未指定にしていたところpostがエラーになってしまい、なかなか原因追求ができませんでした(分かれば当たり前なのですけれど)ちなみに、postの場合の期待値はルートディレクトリにリダイレクトされるのを正としています。XXXControllerTest.kt@KtorExperimentalAPI class XXXControllerTest { val ctrl = APITestController() @Test fun recreate() = ctrl.get("/xxx/aaa") { assertEquals(HttpStatusCode.OK, response.status()) } @Test fun recreatePost() = ctrl.post("/xxx/aaa", listOf("email" to "example@example.com").formUrlEncode(), Headers.build { append(HttpHeaders.ContentType, ContentType.Application.FormUrlEncoded.toString()) }) { assertEquals(HttpStatusCode.Found, response.status()) assertEquals("/", response.headers.get("Location")) } }結果
こんな感じでルーティングの各エンドポイントを網羅することでスモークテストが実現可能になりました!
ハマったこと・課題など
- postのときのヘッダー設定がなかなかわからなかった
- TestApplicationEngine を作成するたびに DB connection数が増えるため上限に気をつけること
参考
- 投稿日:2019-02-03T21:25:24+09:00
【Kotlin】Sequenceオブジェクト生成方法5種
Kotlin の
Sequenceオブジェクトの生成方法を5種紹介します。
asSequence拡張関数
Sequenceを使ったことがある方は皆さんご存知でしょう。
Listインターフェイスなどが継承しているIterableインターフェイスや、ArrayやIntArrayなどの配列系クラス。それらの拡張関数asSequenceを使うことで、それらをSequence化できます。val sequence = listOf('A', 'B', 'C').asSequence() sequence.forEach { print(it) } // > ABCなお、関数名は
toSequenceではなくasSequenceです。
asSequence関数で生成したSequenceオブジェクトには、元となるオブジェクトへの変更が反映されます。val mutableList = mutableListOf('A', 'B') val sequence = mutableList.asSequence() mutableList += 'C' sequence.forEach { print(it) } // > ABC
toList関数などでは、元となるオブジェクトへの変更が反映されません。
この違いが関数名に現れているのでしょう。
sequenceOf関数引数で与えられた要素から
Sequenceオブジェクトを生成します。val sequence = sequenceOf('A', 'B', 'C') sequence.forEach { print(it) } // > ABC
Sequenceインターフェイスの拡張関数
filterやmapなどの、Sequenceオブジェクトを加工して別のSequenceオブジェクトを生成する、Sequenceインターフェイスの拡張関数です。
コレクション系の型の拡張関数と同じ感覚で使えます。val sequence = sequenceOf(0, 1, 2) .map { 'A' + it } sequence.forEach { print(it) } // > ABC
generateSequence関数要素を生成する関数1から
Sequenceオブジェクトを生成します。
関数がnullを生成すると終端するため、要素にnullを含むSequenceオブジェクトは生成できません。
また、生成したSequenceオブジェクトは一度しか使えません。
このような制約があるものの、ある程度自由に手軽にSequenceを作成できます。var char = 'A' val sequence = generateSequence { if (char > 'C') null else char++ } sequence.forEach { print(it) } // > ABC sequence.forEach { print(it) } // IllegalStateException をスローする。
sequence関数本記事で最もお伝えしたい方法です。
任意のSequenceを簡単に作成できます。val sequence = sequence { var char = 'A' while(char <= 'C') { yield(char++) } } sequence.forEach { print(it) } // > ABC sequence.forEach { print(it) } // > ABC使い方
sequence関数の引数である関数(以下、blockと表記)の中でyield関数を呼び出すごとに、yield関数に引数として渡した値が、生成したSequenceオブジェクトの要素となります。
blockの処理が終了すると終端します。
yield関数を呼び出して要素を生成すると、次の要素が要求されるまでblockの処理は停止します。
そのため、終わりがない数列を表すSequenceオブジェクトを生成するような場合でも、必要な計算のみが行われます。解説
blockはSequenceScopeクラスをレシーバーとする拡張関数です。
したがって、block内ではthis.yield(もしくはthis.を省略してyield)とすることで、SequenceScope#yieldメソッドを呼び出せます。
yield関数は中断関数(suspending function)です。
中断関数は、呼び出されてから処理を返すまでの間に処理を「中断」することができます。
「中断」すると、続きの処理は非同期で行われることになります。
中断関数は Kotlin 1.3 で正式に採用された Coroutines の機能に関連するものです。詳しくはこの辺りをご覧ください。→ Coroutines Overview例:フィボナッチ数列
val fibonacci = sequence { var a = 0 var b = 1 while (true) { yield(b) val c = a + b a = b b = c } } fibonacci .take(10) .forEach { println(it) }
出力
1
1
2
3
5
8
13
21
34
55
おわりに
以上、
Sequenceオブジェクトの生成方法を5種紹介しました。
一通り網羅したつもりですが、他にもあればご教授ください。
ラムダ式を使う場合がほとんどでしょう。 ↩
- 投稿日:2019-02-03T17:30:57+09:00
KtorのAutoReloadを有効にしました。
おはようございます。@naokiurです。
よろしくお願い致します。2019年も引き続き、
個人的にKotlin/Ktorに興味を持っている毎日です。AutoReload、便利ですよね。
公式に習い、設定したいと思います。環境
- MacBook Pro (Retina 13-inch、Early 2015)
- macOS High Sierra 10.13.6
- Java 1.8.0_191
- IntelliJ IDEA CE 2018.3.4
実施したこと
- application.confを修正した。
結果
- AutoReloadが有効になった。
AutoReloadの設定
実施方法は2つあります。
embeddedServerに記載するapplication.confに記載する自分の今回のコードでは、
embeddedServerでサーバを起動しておらず、
application.confに設定を記載し、
io.ktor.server.netty.EngineMainを実行することで、
サーバを起動しておりました。そのため、
「application.confに記載する」方法を取りました。application.confktor { deployment { port = 8080 watch = [ out/production ] // ← 追記する } application { modules = [ jp.ne.naokiur.api.ExecutorKt.api ] } }
application.conf内のwatchキーに、
今回のコードのコンパイル出力フォルダを設定します。
IntelliJのデフォルトになっているため、
out/productionを設定しました。これによって、
コンパイル出力フォルダに変更があった場合、
AutoReloadするようになりました。サーバ起動時、以下のようなログが出力されます。
16:36:19.876 [main] DEBUG Application - Watching /絶対パス/out/production/classes/〜 for changes.ただし
コンパイル出力フォルダに変更があった場合なので、
gradle buildや、
IntelliJのcmd + F9でBuild Projectを実施する必要があります。勘違いしたところ
熟読しなかった自分のせいなのですが、
公式にwatch = [ module1, module2 ]と書いてあったため、
最初、今回のコードのモジュール名である、apiを設定してしまいました。これでは有効にならず、
AutoReloadが有効になっていない旨の、
以下のようなログが出力されてしまいました。
16:20:24.636 [main] INFO Application - No ktor.deployment.watch patterns match classpath entries, automatic reload is not activeそのあたりのことは、ちゃんと公式にかかれていました…。
file:///path/to/project/build/classes/myproject.jarというクラスパスならば、
to/projectだとマッチします。
しかしパッケージ名のようなcom.mydomainではマッチしません。今後
DBアクセスを実施したいと思っています。
参考にさせて頂きました
- 投稿日:2019-02-03T17:30:57+09:00
KtorのAutoReloadを有効にする。
おはようございます。@naokiurです。
よろしくお願い致します。2019年も引き続き、
個人的にKotlin/Ktorに興味を持っている毎日です。AutoReload、便利ですよね。
公式に習い、設定したいと思います。環境
- MacBook Pro (Retina 13-inch、Early 2015)
- macOS High Sierra 10.13.6
- Java 1.8.0_191
- IntelliJ IDEA CE 2018.3.4
実施したこと
- application.confを修正した。
結果
- AutoReloadが有効になった。
AutoReloadの設定
実施方法は2つあります。
embeddedServerに記載するapplication.confに記載する自分の今回のコードでは、
embeddedServerでサーバを起動しておらず、
application.confに設定を記載し、
io.ktor.server.netty.EngineMainを実行することで、
サーバを起動しておりました。そのため、
「application.confに記載する」方法を取りました。application.confktor { deployment { port = 8080 watch = [ out/production ] // ← 追記する } application { modules = [ jp.ne.naokiur.api.ExecutorKt.api ] } }
application.conf内のwatchキーに、
今回のコードのコンパイル出力フォルダを設定します。
IntelliJのデフォルトになっているため、
out/productionを設定しました。これによって、
コンパイル出力フォルダに変更があった場合、
AutoReloadするようになりました。サーバ起動時、以下のようなログが出力されます。
16:36:19.876 [main] DEBUG Application - Watching /絶対パス/out/production/classes/〜 for changes.ただし
コンパイル出力フォルダに変更があった場合なので、
gradle buildや、
IntelliJのcmd + F9でBuild Projectを実施する必要があります。勘違いしたところ
熟読しなかった自分のせいなのですが、
公式にwatch = [ module1, module2 ]と書いてあったため、
最初、今回のコードのモジュール名である、apiを設定してしまいました。これでは有効にならず、
AutoReloadが有効になっていない旨の、
以下のようなログが出力されてしまいました。
16:20:24.636 [main] INFO Application - No ktor.deployment.watch patterns match classpath entries, automatic reload is not activeそのあたりのことは、ちゃんと公式にかかれていました…。
file:///path/to/project/build/classes/myproject.jarというクラスパスならば、
to/projectだとマッチします。
しかしパッケージ名のようなcom.mydomainではマッチしません。今後
DBアクセスを実施したいと思っています。
参考にさせて頂きました
- 投稿日:2019-02-03T13:59:46+09:00
AACのViewModelをFactoryクラス不要で簡単に生成できる拡張関数
Androidで、MVVM及びdatabindingを行う場合、Android Architecture ComponentsのViewModelを使うことが多いと思います。
このViewModelを生成する際、コンストラクタに引数が必要だとFactoryクラスを作成する必要がありますが、ViewModelが多くなってくるとその分Factoryも増えていき、作成・管理が面倒になったので拡張関数を作成しました。結論
忙しい人のために先に結論
Fragment.getViewModelとFragment.viewModelProviderを作りました。使い方
class SampleFragment: Fragment(), SampleNavigator { // フィールドでvalで持ちたい場合は、viewModelProvider private val viewModel by viewModelProvider { val str = requireNotNull(arguments?.getString(ARG_SAMPLE_STR)) SampleViewModel(str, this) } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) val str= requireNotNull(arguments?.getString(ARG_SAMPLE_STR)) // 一時的に使用したい(or lateinit varで初期化したい)場合はgetViewModel val vm = getViewModel { SampleViewModel(str, this) } // ActivityのViewModelを参照したい場合 val viewModelOfActivity = requireActivity().getViewModel{ SampleActivityViewModel() } } // 本質外 companion object { private const val ARG_SAMPLE_STR = "ARG_SAMPLE_STR" fun newInstance(str: String) = SampleFragment().apply { arguments = bundleOf(ARG_SAMPLE_STR to str) } } }for Fragment
FragmentExt.ktinline fun <reified VM : ViewModel> Fragment.getViewModel(crossinline factory: () -> VM): VM { return ViewModelProviders.of(this, object : ViewModelProvider.Factory { override fun <VM : ViewModel> create(modelClass: Class<VM>): VM { return requireNotNull(modelClass.cast(factory())) } }).get(VM::class.java) } inline fun <reified VM : ViewModel> Fragment.viewModelProvider(crossinline factory: () -> VM) = lazy { getViewModel { factory() } }for FragmentActivity
上の拡張関数で生やす元がFragmentActivityになっただけ
FragmentActivityExt.ktinline fun <reified VM : ViewModel> FragmentActivity.getViewModel(crossinline factory: () -> VM): VM { return ViewModelProviders.of(this, object : ViewModelProvider.Factory { override fun <VM : ViewModel> create(modelClass: Class<VM>): VM { return requireNotNull(modelClass.cast(factory())) } }).get(VM::class.java) } inline fun <reified VM : ViewModel> FragmentActivity.viewModelProvider(crossinline factory: () -> VM) = lazy { getViewModel { factory() } }解説
Fragment(とFragmentActivity)に拡張関数を生やして、ViewModelProviders.of().get()をいい感じにしてくれればゴールです。
ViewModelProviders.ofに必要なのは
- Fragment
- ViewModelProvider.Factory クラスをもらってViewModelを返すもの
- get(ViewModelのクラス)
Fragmentはthisが使えます。
ViewModelProvider.Factoryの中身は、createを実装するもので、これはViewModelを生成して返せばゴールです。
Factoryは、引数を拡張関数は知らなくて良い(外に任せる)ので、引数無しでVMを返す高階関数で表現できます。
factory: () -> VM
getにはクラスを入れる必要があります。
拡張関数の引数でもらってきて入れるのが簡単ですが、VMはViewModelを継承したクラスと分かっているので、VM::class.javaができればクラスを引数に取る必要はありません。
そこでreifiedの登場です。reified
3行でまとめると
reified type paramter = 具象化された型パラメータ
→reifiedを使うことで、T::class.javaを実現できる
→reifiedを使うには、inline関数である必要がある公式サイトや太郎さんの記事が参考になります。
reifiedを使えば良いですが、このためにはinline関数にする必要があります。
ただ、crossinlineをつけないとfactory: () -> VMがcreate内から参照できません。crossinline
まとめると
inline関数では、渡されてラムダを関数本体から直接ではなく、
ローカルオブジェクトやネストされた関数などの別の実行コンテキストから呼び出すことは許可されていない
→ラムダパラメータにcrossinlineをつけることで可能になる詳しくは公式サイトのreifiedの上に載っています。
requireNotNull(modelClass.cast(factory()))
は警告回避のために行っています。
as TのUNCHECKED_CASTの回避のためにmodelClass.cast(factory())
NULLABILITY_MISMATCH_BASED_ON_JAVA_ANNOTATIONSの回避のためにrequireNotNull以上を組み合わせることで拡張関数を実現しています。
調べていたとき、reifiedが便利すぎて感動しました。
T::class.javaが出来るなんてジェネリクス好き歓喜。道行き
結論に至るまでの道のりを備忘録がてら記載。
ViewModelProvidersの利用
通常、AACのViewModelを取得するためには、ViewModelProviders.ofを利用します。
// 引数がない場合 ViewModelProviders.of(Fragment).get(SampleViewModel::class.java) // 引数が必要な場合 ViewModelProviders.of(Fragment, SampleViewModelFactory(arg1, arg2)).get(SampleViewModel::class.java)多くの場合、引数が必要な場合はViewModelクラスと対になるFactoryClassを作ります。
SampleViewModelFactory.ktclass SampleViewModelFactory(arg1: String, arg2: Int) : ViewModelProvider.Factory { override fun <T : ViewModel?> create(modelClass: Class<T>): T { if (modelClass != SampleViewModel.class.java) { throw IllegalArgumentException("ViewModel class is not SampleViewModel") } return TextToSpeechDemoViewModel(arg1, arg2) as T } }Util化
毎回クラスを作るのは面倒です。
引数を見てみると、必要なのはViewModelProder.Factoryとなっています。
Factoryクラスを作るときに、実装しましたね。要はこれがあればよいのです。
リファレンスFactoryの中にはcreateメソッドがあり、中を見てみるとクラスをもらって引数を利用してViewModelを生成し、Tにキャストして返しています。
ViewModelによって必要な引数はバラバラです。
なのでViewModelを生成する処理は引数にとり外に任せることにしましょう。
ViewModelを生成する処理を高階関数() -> Tとして引数に渡せば、生成ロジックを外に任せることができます。
これでUtilクラスで汎用的にできそうです。ViewModelProvidersUtils.kt// 引数がない場合の生成メソッド fun <T : ViewModel> create(fragment: Fragment, clazz: Class<out T>): T { return ViewModelProviders.of(fragment, object : ViewModelProvider.Factory { override fun <T : ViewModel> create(modelClass: Class<T>): T { return clazz.newInstance() as T } }).get(clazz) } // 引数がある場合の生成メソッド fun <T : ViewModel> create(fragment: Fragment, clazz: Class<out T>, factory: () -> T): T { return ViewModelProviders.of(fragment, object : ViewModelProvider.Factory { override fun <T : ViewModel> create(modelClass: Class<T>): T { return factory() as T } }).get(clazz) }使う側
// 引数なし val vm = ViewModelProvidersUtils.createViewModel(SampleViewModel::class.java) // 引数あり val vm = ViewModelProvidersUtils.createViewModel(SampleViewModel::class.java) { SampleViewModel(arg1, arg2) }Utilの2つのメソッドの実装をよく見ると、ほとんど同じ処理をしています。
引数一つのcreateViewModelは、引数二つのcreateViewModelのfactory() as Tに当たる部分でclazz.newInstance()をしていますね。
kotlinはデフォルト引数が使えるので一つにできます。ViewModelProvidersUtils.ktfun <T : ViewModel> create(fragment: Fragment, clazz: Class<out T>, factory: () -> T = { clazz.newInstance() as T }): T { return ViewModelProviders.of(fragment, object : ViewModelProvider.Factory { override fun <T : ViewModel> create(modelClass: Class<T>): T { return factory() as T } }).get(clazz) }これで、
第二引数を指定しない場合はclazz.newInstance()が行われる
指定すれば引数ありの場合に対応できる
ようになりました。拡張関数化
必ずFragmentから呼ぶもので、汎用的に使えそうなので拡張関数にしてみましょう。
FragmentExt.ktfun <T : ViewModel> Fragment.createViewModel(clazz: Class<out T>, factory: () -> T = { clazz.newInstance() as T }): T { return ViewModelProviders.of(this, object : ViewModelProvider.Factory { override fun <T : ViewModel> create(modelClass: Class<T>): T { return factory() as T } }).get(clazz) }使う側
Fragment内からの呼び出し// 引数なし val vm = createViewModel(SampleViewModel::class.java) // 引数あり val vm = createViewModel(SampleViewModel::class.java) { SampleViewModel(arg1, arg2) }ここで拡張関数を見てみると、(人間的には)返す型がTと決まっているので、クラスを引数に渡すのは無駄じゃない?と思います。
.get(clazz)のところをT::class.javaとできれば、clazz: Classを無くせそうです。
そこで、reifiedが登場し、最終形になります。
解説へと続くわけです
