- 投稿日:2019-05-12T21:06:09+09:00
GoogleのKotlin style guideを翻訳してみた
Googleが 公開している Kotlinのスタイルガイドを日本語に翻訳してみました。
訳が間違っているところがあったら教えてくださいKotlin スタイルガイド
この文書はKotlinのソースコード用Androidコーディング標準の完全な定義です。Kotlinのソースファイルは、この規則に準拠している場合に限り、Google Android Styleであるとしてみなされます。
他のプログラミングスタイルガイドと同様に、カバーされている問題はフォーマットの美しさだけでなく、他の種類の規約・コーディング規約にも及んでいます。ただし、この文書では主に私たちが普遍的に従う厳格な規則に焦点を当てていて、人・ツールに関わらず、明確でないアドバイスが含まれないようにしています。
最終更新日: 2018年5月18日
ソースファイル
全てのソースファイルはUTF-8でエンコードされている必要があります。
命名
ソースファイルにトップレベルクラスが1つしか含まれていない場合、ファイル名は大文字と小文字を区別して、クラス名に
.kt拡張子を付けたものにします。複数のトップレベルクラスが含まれている場合は、ファイルの内容が分かる名前をつけて、それをPascalCaseにして
.kt拡張子を付けます。// MyClass.kt class MyClass { }// Bar.kt class Bar { } fun Runnable.toBar(): Bar = // …// Map.kt fun <T, O> Set<T>.map(func: (T) -> O): List<O> = // … fun <T, O> List<T>.map(func: (T) -> O): List<O> = // …特殊文字
空白文字
ソースファイルに現れる空白文字は、改行コードを除くと ASCII 水平スペース (0x20) のみを使います。
- 文字列・文字リテラル内の他の全ての空白文字はエスケープします
- インデントにはタブ文字を使いません
エスケープシーケンス
特別なエスケープシーケンスがある全ての文字(
\b\n\r\t\'\"\\\$)は、対応するUnicodeエスケープ(例:\u000a) ではなく、そのエスケープシーケンスを使います。非ASCII文字
非ASCII文字については、実際のUnicode文字(例:
∞) またはUnicodeエスケープ(例:\u221e) の両方が使えます。どちらを使うかは どちらがコードが読みやすい・理解しやすいようになるか の観点から決めます。Unicodeエスケープは、常に出力可能文字には使用しないことを推奨します。また、文字列リテラルやコメントの外での使用は推奨されません。
例 評価 val unitAbbrev = "μ's"最高: コメントがなくても完全に明確です val unitAbbrev = "\u03bcs" // μs悪い: 出力可能文字でエスケープを使う理由はありません val unitAbbrev = "\u03bcs"悪い: 読む人はこれが何か分かりません return "\ufeff" + content良い: 出力できない文字にはエスケープを使い、必要ならコメントをつけてください 構造
.ktファイルは順番に以下のセクションを含みます
- 著作権/ライセンスのヘッダ(オプション)
- ファイルレベルのアノテーション
- package文
- import文
- トップレベルの宣言
これらの各セクションは1つの空白行で区切られます。
著作権/ライセンスのヘッダ
著作権ヘッダやライセンスヘッダがファイルについている場合は、複数行コメントのすぐ上に記載する必要があります
OK/* * Copyright 2017 Google, Inc. * * ... */KDocスタイル や単一行スタイルのコメントは使用しないでください。
NG (KDocスタイルのコメント)/** * Copyright 2017 Google, Inc. * * ... */NG (単一行スタイルのコメント)// Copyright 2017 Google, Inc. // // ...ファイルレベルのアノテーション
"file"ユーズサイトターゲットのアノテーションはヘッダーコメントとパッケージ宣言の間に配置します。
package文
package文は1行あたりの最大文字数制限を受けず、行は折り返されません。
import文
クラス・関数・プロパティのimport文は1つのリストにまとめられ、ASCII順にソートされます。
ワイルドカードを用いたimportはどのような種類の物でも 許可されていません。
package文と同様に、import文は1行あたりの最大文字数制限を受けず、行は折り返されません。トップレベルの宣言
.ktファイルでは、トップレベルで1つ以上の型・関数・プロパティ・型エイリアスを宣言することができます。
ファイルの内容は1つのテーマに関しての内容である必要があります。例として、1つのpublic型や、複数のレシーバーで同じ操作を実行する拡張機能をまとめたものがあります。関係のない宣言は別のファイルに分けられるべきであり、1つのファイルに含まれるpublic宣言は少なくするべきです。
ファイルに含まれる内容の数や順序に明確な制限はありません。
通常、ソースファイルは上から下に向かって読まれます。つまり、一般的に上にある宣言ほど内容が良く理解されます。ファイルが異なれば、内容の順序を変えることができます。同様に、1つのファイルに100個のプロパティ・10個の関数・さらに1つのクラスを含めることができます。それぞれのクラスが 何らかの 論理的な順序に沿って並べることが重要です。論理的な順序とは、メンテナに要求されたら説明することができるものです。例えば、新しい関数をクラスの最後に追加する習慣を使ってはいけません。論理的な順序ではない、「追加された日付順」で並べられるからです。
クラスメンバーの順序
クラス内のメンバーの順序は、トップレベルの宣言と同じ規則に従って並べます。
フォーマット
中括弧
他に
else ifやif分岐がなく、1行におさまるwhen分岐やif文の中身には、中括弧は必要ありません。if (string.isEmpty()) return when (value) { 0 -> return // … }それ以外の場合は、中身が空や1つの文しか含まれていない場合でも、
if・for・when分岐・do・while文には中括弧が必要です。if (string.isEmpty()) return // NG if (string.isEmpty()) { return // OK }空でないブロック
空でないブロックやブロックのような構成要素での中括弧は、K&Rスタイル(エジプト括弧)に従います。
- 開き中括弧の前に改行を置きません
- 開き中括弧の後で改行します
- 閉じ中括弧の前に改行します
- 閉じ中括弧の後の改行は、その中括弧が文を終了するか、関数・コンストラクタ・名前付きクラスの本体を終了する場合のみ置きます。例えば、中括弧の後に
elseやカンマが続く場合は、中括弧の後に改行を置きませんreturn Runnable { while (condition()) { foo() } } return object : MyClass() { override fun foo() { if (condition()) { try { something() } catch (e: ProblemException) { recover() } } else if (otherCondition()) { somethingElse() } else { lastThing() } } }enumクラスにはいくつかの例外があります。
空のブロック
空のブロックやブロックのような構成要素はK&Rスタイルでなければなりません。
try { doSomething() } catch (e: Exception) {} // NGtry { doSomething() } catch (e: Exception) { } // OK式
式として使用される
if/else条件は、式全体が1行におさまる場合にのみ中括弧を省略することができます。val value = if (string.isEmpty()) 0 else 1 // OKval value = if (string.isEmpty()) // NG 0 else 1val value = if (string.isEmpty()) { // OK 0 } else { 1 }インデント
ブロックやブロックのような構成要素が開かれるたびに、スペース4つ分インデントします。ブロックが終了すると、インデントは前のインデントレベルに戻ります。インデントレベルはブロック全体のコードとコメントの両方に適用されます。
1行に1つの文
それぞれの文の後には改行が続きます。セミコロンは使用しません。
行の折り返し
コードの最大桁数は100文字です。以下の場合を除き、100文字を超える行は後述のように行を折り返す必要があります。
例外
* 100文字制限に従うことが不可能な行(例: KDocの長いURL)
*package・import文
* シェルにコピペできるコマンドを書いたコメント折り返す位置
行の折り返しの主な条件は次の通りです。より高い構文レベルで分割することを優先します。
=以外の演算子で行が分割される場合は、その演算子の直前で折り返します
- これは次の「演算子のような記号」にも当てはまります
- ドット区切り記号 (
.)- メンバー参照で使う2つのコロン (
::)=で行が分割される場合は、=の直後で折り返します- メソッド名やコンストラクタ名はその直後の開き括弧 (
()と分割することはできません- カンマ (
,)はその前のトークンに付加されたままです- ラムダ矢印 (
->)は、その前の引数リストについたままです注
行の折り返しの主な目的は、簡潔なコードを書くことです。それは必ずしも最小の行数に収まるコードを書くことと等価であるとは限りません。
関数
関数シグネチャが1行におさまらない場合は、各パラメータ宣言をそれぞれ1行に分割してください。このとき、それぞれのパラメータは1段階インデント(スペース4つ)するべきです。閉じ括弧 (
)) と戻り値の型はインデントを増やさず、それぞれ別の行に配置されます。fun <T> Iterable<T>.joinToString( separator: CharSequence = ", ", prefix: CharSequence = "", postfix: CharSequence = "" ): String { // … }単一式関数
関数に1つの式しか含まれていない場合は、単一式関数として書くことができます。
通常の例override fun toString(): String { return "Hey" }単一式関数を使った例override fun toString(): String = "Hey"ブロックを開いたときのみ単一式関数が複数に折り返されます。
fun main() = runBlocking { // … }ブロックを開いたとき以外で単一式関数が折り返しを必要とするようになった場合は、
returnを使った通常の関数として書き、通常の折り返し規則を使用してください。プロパティ
プロパティ初期化子が1行におさまらない場合は、イコール(
=)の後で改行し、インデントしてください。private val defaultCharset: Charset? = EncodingRegistry.getInstance().getDefaultCharsetForPropertiesFiles(file)関数
get/setを宣言するプロパティは、1段階インデント(スペース4つ)して、それぞれを独自の行に配置します。関数と同じ規則を使用します。var directory: File? = null set(value) { // … }読み取り専用プロパティは1行におさまる短い構文を使用できます。
val defaultExtension: String get() = "kt"空白
垂直方向
次の場合は空白行を1行おきます
- プロパティ・コンストラクタ・関数・子クラスなどの、クラス内の連続するメンバー間
- 例外: 2つの連続したプロパティ間の空白行はオプションです。このような空白行は必要に応じてプロパティを論理的にグループ化したり、存在する場合はプロパティをそのバッキングプロパティに関連付けたりするために使われます。
- 例外: 列挙型定数の間の空白行は下記の通りです。
- 必要に応じて文を論理的なサブセクションに分割するため
- 必要に応じて関数の最初の文の前、クラスの最初のメンバーの前、クラスの最後のメンバーの後 (推奨ではありません)
- この文書の他のセクション(構造セクションなど)の内容に応じて
複数の連続した空白行は許容されますが、推奨されることも要求されることもありません。
水平方向
言語仕様や他のスタイル規則で要求されている場所・リテラル内・コメント・KDocを除いて、単一のASCIIスペースは次の場所にのみ置かれます。
if・for・catchなどの任意の予約語を、その後に続く開き中括弧({)から分離するため// NG for(i in 0..1) { }// OK for (i in 0..1) { }
else・catchなどの任意の予約語を、その前にある閉じ中括弧(})から分離するため// NG }else { }// OK } else { }
- 任意の開き中括弧(
{)の前// NG if (list.isEmpty()){ }// OK if (list.isEmpty()) { }
- 二項演算子の左右
// NG val two = 1+1// OK val two = 1 + 1これは次の「演算子のような記号」にも当てはまります
- ラムダ式の矢印(
->)// NG ints.map { value->value.toString() }// OK ints.map { value -> value.toString() }しかし、以下の場合は当てはまりません
- メンバー参照で使う2つのコロン(
::)// NG val toString = Any :: toString// OK val toString = Any::toString
- ドット区切り記号(
.)// NG it . toString()// OK it.toString()// OK val toString = Any::toString
- 範囲演算子(
..)// NG for (i in 1 .. 4) print(i)// OK for (i in 1..4) print(i)
- コロン(
:)の前は基本クラスまたは基本インターフェースを指定するためのクラス宣言、またはジェネリクスのwhere説で使用されている場合に限ります// NG class Foo: Runnable// OK class Foo : Runnable// NG fun <T: Comparable> max(a: T, b: T)// OK fun <T : Comparable> max(a: T, b: T)// NG fun <T> max(a: T, b: T) where T: Comparable<T>// OK fun <T> max(a: T, b: T) where T : Comparable<T>
- カンマ(
,)・コロン(:)の後// NG val oneAndTwo = listOf(1,2)// OK val oneAndTwo = listOf(1, 2)// NG class Foo :Runnable// OK class Foo : Runnable
- 1行コメントを開始する二重スラッシュ(
//)の両側。ここでは複数のスペースを使用できますが、必須ではありません。// NG var debugging = false//disabled by default// OK var debugging = false // disabled by defaultこの規則は、行頭または行末にスペースを書くことを要求したり禁止したりするわけではありません。内部空間のみを対象とします。
特定の文脈
enumクラス
関数・その定数に関するドキュメントがないenumは、1行で書くことができます。
enum class Answer { YES, NO, MAYBE }enum内の定数が別々の行に配置されている場合、それらが本体を定義する場合を除いて、空白行は必要ありません。
enum class Answer { YES, NO, MAYBE { override fun toString() = """¯\_(ツ)_/¯""" } }enumクラスはクラスなので、クラスの書式に関する他の全ての規則が適用されます。
アノテーション
メンバーまたは型のアノテーションは対象要素の直前の別の行に配置されます。
@Retention(SOURCE) @Target(FUNCTION, PROPERTY_SETTER, FIELD) annotation class Global引数がないアノテーションは単一行に配置できます。
@JvmField @Volatile var disposable: Disposable? = null引数がないアノテーションが1つしかない場合は、宣言と同じ行にアノテーションを配置することができます。
@Volatile var disposable: Disposable? = null @Test fun selectAll() { // … }暗黙の戻り値/プロパティ型
単一式関数本体またはプロパティの初期化子がスカラー値である場合・戻り値が明確に推測できる場合は、その型を省略できます。
override fun toString(): String = "Hey" // ↓ override fun toString() = "Hey"private val ICON: Icon = IconLoader.getIcon("/icons/kotlin.png") // ↓ private val ICON = IconLoader.getIcon("/icons/kotlin.png")ライブラリを作成するとき、それがパブリックAPIの一部である場合は型を明示的に指定してください。
命名
識別子にはASCII文字と数字だけを使用します。また、後述の一部のケースでは追加でアンダースコア(
_)も使用します。従って、有効な識別子名は正規表現\w+で表現されます。
name_,mName,s_name,kNameなどの例に見られるような特別な接頭辞や接尾辞はバッキングプロパティを除いて使用されません。パッケージ名
パッケージ名はすべて小文字で連続する単語はアンダースコアなしで単純に連結されます。
// OK package com.example.deepspace // NG package com.example.deepSpace // NG package com.example.deep_space型名
クラス名はPascalCaseで書かれ、通常は名詞または名詞句です。(例:
Character,ImmutableList)。インターフェース名は名詞や名詞句(例:List)でもかまいませんが、形容詞や形容詞句(例:Readable)を使うこともあります。
テストクラスはテストしているクラスの名前で始まり、Testで終わる名前がつけられます。(例:HashTest,HashIntegrationTest)関数名
関数名はcamelCaseで書かれ、通常は動詞または動詞句です。(例:
sendMessage,stop)。
テスト関数名の論理構成要素を区切るためにアンダースコア(_)を使用できます。@Test fun pop_emptyStack() { // … }定数名
定数名ではUPPER_SNAKE_CASEを使用します。すべて大文字で、単語はアンダースコア(
_)で区切ります。しかし、本当は何が定数なのでしょうか?
定数はvalプロパティで、カスタムget関数を持たず、その内容は完全に不変です。関数には検出できる副作用はありません。これには、不変な型、不変な型への不変なコレクション、constとしてマークされているスカラー型と文字列が含まれます。インスタンスで、状態のいずれかが変化する可能性があり、それを検出できる場合は、それは定数ではありません。オブジェクトを絶対に変更しないようにするだけでは不十分です。const val NUMBER = 5 val NAMES = listOf("Alice", "Bob") val AGES = mapOf("Alice" to 35, "Bob" to 32) val COMMA_JOINER = Joiner.on(',') // Joiner is immutable val EMPTY_ARRAY = arrayOf()定数の名前は通常は名詞または名詞句です。
定数値はobjectの内部またはトップレベルの宣言としてのみ定義できます。それ以外の場合は定数の要件を満たしていても、定数の名前は使用できません。
スカラー値である定数はconst修飾子を使わなければなりません。非定数名
非定数名はcamelCaseで書かれます。これらはインスタンスプロパティ・ローカルプロパティ・パラメータ名に適用されます。
val variable = "var" val nonConstScalar = "non-const" val mutableCollection: MutableSet = HashSet() val mutableElements = listOf(mutableInstance) val mutableValues = mapOf("Alice" to mutableInstance, "Bob" to mutableInstance2) val logger = Logger.getLogger(MyClass::class.java.name) val nonEmptyArray = arrayOf("these", "can", "change")これらの名前は通常は名詞または名詞句です。
バッキングプロパティ
バッキングプロパティが必要とされるときには、その名前はアンダースコア(
_)で始まることを除いて必ず実際のプロパティ名と一致する必要があります。private var _table: Map? = null val table: Map get() { if (_table == null) { _table = HashMap() } return _table ?: throw AssertionError() }型変数名
型変数は次の2スタイルのどちらかによって名前がつけられます。
- 1文字の大文字に必要に応じて1文字の数字が後に続く(例:
E,T,X,T2)- クラス名に使われる名前のあとに
Tをつける(例:RequestT,FooBarT)キャメルケース
頭字語や「IPv6」・「iOS」のような変わった構成の単語が存在する場合など、英語のフレーズをキャメルケースに変換するための方法が複数あることがあります。予測可能性を向上させるために、以下の方法を使用してください。
散文形式の名前から始めます。
- フレーズをプレーンなASCIIに変換し、アポストロフィを削除します。例えば「Müller’s algorithm」は「Muellers algorithm」になります。
- この結果をスペースと残りの句読点(主にハイフン)で単語に分割します。
推奨:
一般的に使用されている単語の中に従来のキャメルケースの単語がある場合は、これを構成部分に分割します(例: "AdWords"→"ad words")
「iOS」などの単語はそれ自体はキャメルケースではありません。この場合は他のどのような慣習にも反するので、このルールは適用されません。- 頭字語を含む、全ての文字を小文字にしてから、次のいずれかを実行します
- パスカルケースにする場合は、各単語の最初の文字を大文字にする
- キャメルケースにする場合は、最初の単語を除いて、各単語の最初の文字を大文字にする
- 最後に、すべての単語を1つの識別子に結合します
元の単語の大文字と小文字の区別はほとんど無視されることに注意してください。
元の単語 正解 間違い XML Http Request XmlHttpRequest XMLHTTPRequest new customer ID newCustomerId newCustomerID inner stopwatch innerStopwatch innerStopWatch supports IPv6 on iOS supportsIpv6OnIos supportsIPv6OnIOS YouTube importer YouTubeImporter YoutubeImporter 1 注
いくつかの単語は英語では曖昧なハイフンで連結されています。例えば、"noempty"と"non-empty"はどちらも正しい英語です。よってメソッド名も
checkNoemptyとcheckNonEmptyのどちらも正しいです。ドキュメンテーション
書式
KDocブロックの基本的な書式はこのような形です
/** * Multiple lines of KDoc text are written here, * wrapped normally… */ fun method(arg: String) { // … }またはこのような1行で書くことができます
/** An especially short bit of KDoc. */この基本形は常に受け入れ可能です。KDocブロック全体(コメントマーカーを含む)が1行におさまる場合は、単一行形式に置き換えることができます。単一行形式は
@returnのようなブロックタグがない場合にのみ使えることに注意してください。段落
1行の空白行(最初のアスタリスク(
*)のみを含む行)は段落の区切り・サマリーフラグメントとブロックタグの間に書かれます。ブロックタグ
ブロックタグは
@constructor,@receiver,@param,@property,@return,@throws,@seeの順番で書かれます。説明文が空の場合は表示されません。ブロックタグが1行におさまらない場合は、2行目以降は@の位置からスペース4つインデントします。サマリーフラグメント
KDocブロックは簡単な要約を書くフラグメントで始まります。このフラグメントは非常に重要です。ドキュメントでのクラスやメソッドの目次などの特定のコンテキストで現れる唯一のテキストです。
これは名詞句または動詞句の断片で、完全な分ではありません。Afoois a...やThis method returns...で始まっているわけでも、Save the record.のような完全な命令文を形成する必要もありません。ただし、フラグメントは大文字・小文字が区別され、完全文と同様に読まれます。使い方
最低でも、KDocは全ての
publicな型、以下に述べるいくつかの例外を除いたpublic・protectedなメンバーに関して書く必要があります。例外: 自明な関数
getFooやそのようなプロパティに代表される、単純で明白な関数
典型的な読者が知る必要があるかもしれない関連情報を省略することを正当化するためにこの例外を用いることは適切ではありません。たとえばgetCanonicalName関数やcanonicalNameプロパティの場合、一般的な読者が「CanonicalName」という用語が何を意味するか分からない場合は、そのドキュメントを省略したり、/** Returns the canonical name. */などのKDocを書かないでください。例外: オーバーライド
スーパータイプメソッドをオーバーライドするメソッドに常にKDocがあるわけではありません。
許容されますが推奨されません ↩
- 投稿日:2019-05-12T19:00:02+09:00
Kotlin Coroutinesで非同期並列処理をする
非同期な処理を並列させて、それぞれが完了するまで待ち合わせます。onClick() を実行すると処理が始まります。
コード
並列処理の結果を使って処理を行います。
MainActivityViewModel.ktclass MainActivityViewModel : ViewModel(), LifecycleObserver { private val job = Job() private val scope = CoroutineScope(Dispatchers.Main + job) fun onClick() { scope.launch(Dispatchers.IO) { val t1 = async { task1() } val t2 = async { task2() } Timber.d("onComplete: %d", t1.await() + t2.await()) } } private fun task1(): Int { Timber.d("task 1 START") Thread.sleep(2000) Timber.d("task 1 END") return 1 } private fun task2(): Int { Timber.d("task 2 START") Thread.sleep(3000) Timber.d("task 2 END") return 2 } override fun onCleared() { super.onCleared() job.cancel() }実行結果
task 1 START
task 2 START
task 1 END
task 2 END
onComplete: 3RxKotlin版もありますので参考にしてください。状況に応じてRxとcoroutinesを使い分けると良いでしょう。
RxKotlinで非同期並列処理をする - Qiita
- 投稿日:2019-05-12T18:41:27+09:00
RxKotlinで非同期並列処理をする
非同期な処理を並列させて、それぞれが完了するまで待ち合わせます。
onClick()を実行すると処理が始まります。並列処理をただ待ち合わせる場合(戻り値不要)
mergeオペレータを使います。アイテムは何も流さずにonCompleteで処理を行います。
MainActivityViewModel.ktclass MainActivityViewModel : ViewModel(), LifecycleObserver { private val compositeDisposable = CompositeDisposable() fun onClick() { Observable.merge(task1(), task2()) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe({}, {}, { Timber.d("onComplete") // do something... }) .addTo(compositeDisposable) } private fun task1(): Observable<Unit> { return Completable.fromAction { Timber.d("task 1 START") Thread.sleep(2000) Timber.d("task 1 END") }.subscribeOn(Schedulers.io()).toObservable() } private fun task2(): Observable<Unit> { return Completable.fromAction { Timber.d("task 2 START") Thread.sleep(3000) Timber.d("task 2 END") }.subscribeOn(Schedulers.io()).toObservable() } override fun onCleared() { super.onCleared() compositeDisposable.clear() } }実行結果
task 1 START
task 2 START
task 1 END
task 2 END
onComplete並列処理の結果を使う場合
zipオペレータを使います。
MainActivityViewModel.ktclass MainActivityViewModel : ViewModel(), LifecycleObserver { private val compositeDisposable = CompositeDisposable() fun onClick() { Observables.zip(task1(), task2()) { t1, t2 -> t1 + t2 } .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe { Timber.d("onComplete: %d", it) // do something... } .addTo(compositeDisposable) } private fun task1(): Observable<Int> { return Single.create<Int> { emitter -> Timber.d("task 1 START") Thread.sleep(2000) Timber.d("task 1 END") emitter.onSuccess(1) }.subscribeOn(Schedulers.io()).toObservable() } private fun task2(): Observable<Int> { return Single.create<Int> { emitter -> Timber.d("task 2 START") Thread.sleep(3000) Timber.d("task 2 END") emitter.onSuccess(2) }.subscribeOn(Schedulers.io()).toObservable() } override fun onCleared() { super.onCleared() compositeDisposable.clear() } }実行結果
task 1 START
task 2 START
task 1 END
task 2 END
onComplete: 3RxKotlinでzipを使う場合は
ObservableではなくてObservablesを使うとBiFunction<>を省略することができます。Kotlin Coroutines版もありますので参考にしてください。状況に応じてRxとcoroutinesを使い分けると良いでしょう。
Kotlin Coroutinesで非同期並列処理をする - Qiita
- 投稿日:2019-05-12T03:23:52+09:00
SpringMockKを使ってみた
kotlinのmockライブラリで有名なMockK。便利ですよね。
MockitoのようにSpringに対応してくれればもっと便利なのになあと思っていたら、SpringMockKというそのものずばりなライブラリがあったので試してみました。準備
Spring Initializrで作ったひな型に以下のようなサービスを追加します。
テストではこのサービスをmockにします。HogeService.ktpackage com.example.demo.service import org.springframework.stereotype.Service @Service class HogeService { fun hoge() = "HogeService.hoge()" fun fuga() = "HogeService.fuga()" }また、HogeServiceを利用するコンポーネントも作成します。
Main.ktpackage com.example.demo import com.example.demo.service.HogeService import org.springframework.stereotype.Component @Component class Main( private val hogeService: HogeService ) { fun main() = "${hogeService.hoge()}, ${hogeService.fuga()}" }build.gradleのdependenciesに以下を追加します。
com.ninja-squad:springmockkだけでなくnet.bytebuddy:byte-buddyも追加しないと動きませんでした。build.gradledependencies { // ...略... // SpringMockK testImplementation 'net.bytebuddy:byte-buddy:1.9.12' testImplementation 'com.ninja-squad:springmockk:1.1.2' // ...略... }MockkBean
以下のようにmock化したいサービスに
@MockkBeanアノテーションを付与するとよしなにmock化してくれます。
ちゃんとverifyもできますね。SpringMockKTest.ktpackage com.example.demo.service import com.example.demo.Main import com.ninjasquad.springmockk.MockkBean import io.mockk.every import io.mockk.verify import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.ExtendWith import org.springframework.beans.factory.annotation.Autowired import org.springframework.boot.test.context.SpringBootTest import org.springframework.test.context.junit.jupiter.SpringExtension @ExtendWith(SpringExtension::class) @SpringBootTest class SpringMockKTest { @MockkBean private lateinit var hogeService: HogeService @Autowired private lateinit var main: Main @Test fun mainTest() { every { hogeService.hoge() } returns "SpringMockKHoge.hoge()" every { hogeService.fuga() } returns "SpringMockKHoge.fuga()" assertEquals("SpringMockKHoge.hoge(), SpringMockKHoge.fuga()", main.main()) verify { hogeService.hoge() hogeService.fuga() } } }SpykBean
@SpykBeanアノテーションでspyもできます。SpringMockKSpyTest.ktpackage com.example.demo.service import com.example.demo.Main import com.ninjasquad.springmockk.MockkBean import com.ninjasquad.springmockk.SpykBean import io.mockk.every import io.mockk.verify import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.ExtendWith import org.springframework.beans.factory.annotation.Autowired import org.springframework.boot.test.context.SpringBootTest import org.springframework.test.context.junit.jupiter.SpringExtension @ExtendWith(SpringExtension::class) @SpringBootTest class SpringMockKSpyTest { @SpykBean private lateinit var hogeService: HogeService @Autowired private lateinit var main: Main @Test fun mainTest() { every { hogeService.hoge() } returns "SpringMockKHoge.hoge()" assertEquals("SpringMockKHoge.hoge(), HogeService.fuga()", main.main()) verify { hogeService.hoge() hogeService.fuga() } } }おまけ
SpringMockKを利用せずにSpringでMockKを利用したい場合、以下の方法があるようです。
ただし、うまく動くのはバージョンが2.0.9.RELEASEまでで、Spring Initializrで生成したバージョン2.1.4.RELEASEではエラーで動きませんでした。MockKTest.ktpackage com.example.demo.service import com.example.demo.Main import io.mockk.every import io.mockk.mockk import io.mockk.verify import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.ExtendWith import org.springframework.beans.factory.annotation.Autowired import org.springframework.boot.test.context.SpringBootTest import org.springframework.boot.test.context.TestConfiguration import org.springframework.context.annotation.Bean import org.springframework.test.context.junit.jupiter.SpringExtension // Springのバージョンが2.0.9.RELEASE までなら動く @ExtendWith(SpringExtension::class) @SpringBootTest class MockKTest { @TestConfiguration class MockKConfig { @Bean fun hogeService() = mockk<HogeService>() } @Autowired private lateinit var hogeService: HogeService @Autowired private lateinit var main: Main @Test fun mainTest() { every { hogeService.hoge() } returns "MockKHoge.hoge()" every { hogeService.fuga() } returns "MockKHoge.fuga()" assertEquals("MockKHoge.hoge(), MockKHoge.fuga()", main.main()) verify { hogeService.hoge() hogeService.fuga() } } }参考