Upgrade to Pro — share decks privately, control downloads, hide ads and more …

自動生成でさくさく実装するユニットテスト / quickly implement unit tests with automatic generation

tkmnzm
March 16, 2020

自動生成でさくさく実装するユニットテスト / quickly implement unit tests with automatic generation

tkmnzm

March 16, 2020
Tweet

More Decks by tkmnzm

Other Decks in Technology

Transcript

  1. @TestFactory fun length(): Collection<DynamicTest> { data class TestCase( val text:

    String, val expect: Int ) return listOf<TestCase>( TestCase("test", 4), TestCase("cat", 3) ).map { case -> dynamicTest(case.toString()) { assertThat(case.text.length, equalTo(case.expect)) } } } 作りたいコードの例
  2. @TestFactory fun length(): Collection<DynamicTest> { data class TestCase( val text:

    String, val expect: Int ) return listOf<TestCase>( TestCase("test", 4), TestCase("cat", 3) ).map { case -> dynamicTest(case.toString()) { assertThat(case.text.length, equalTo(case.expect)) } } } DynamicTestのシグネチャ
  3. @TestFactory fun length(): Collection<DynamicTest> { data class TestCase( val text:

    String, val expect: Int ) return listOf<TestCase>( TestCase("test", 4), TestCase("cat", 3) ).map { case -> dynamicTest(case.toString()) { assertThat(case.text.length, equalTo(case.expect)) } } } スコープがFunction内の テストパラメータを保持 するdata class
  4. @TestFactory fun length(): Collection<DynamicTest> { data class TestCase( val text:

    String, val expect: Int ) return listOf<TestCase>( TestCase("test", 4), TestCase("cat", 3) ).map { case -> dynamicTest(case.toString()) { assertThat(case.text.length, equalTo(case.expect)) } } } テストパラメータを 保持するdata classのリスト
  5. @TestFactory fun length(): Collection<DynamicTest> { data class TestCase( val text:

    String, val expect: Int ) return listOf<TestCase>( TestCase("test", 4), TestCase("cat", 3) ).map { case -> dynamicTest(case.toString()) { assertThat(case.text.length, equalTo(case.expect)) } } } DynamicTest型にmap
  6. @TestFactory fun length(): Collection<DynamicTest> { data class TestCase( val text:

    String, val expect: Int ) return listOf<TestCase>( TestCase("test", 4), TestCase("cat", 3) ).map { case -> dynamicTest(case.toString()) { assertThat(case.text.length, equalTo(case.expect)) } } } 1DynamicTestが 1テストケースとして 実行される
  7. @TestFactory fun length(): Collection<DynamicTest> { data class TestCase( val text:

    String, val expect: Int ) return listOf<TestCase>( TestCase("test", 4), TestCase("cat", 3) ).map { case -> dynamicTest(case.toString()) { assertThat(case.text.length, equalTo(case.expect)) } } } data classのtoStringを テストケース名に指定
  8. @TestFactory fun length(): Collection<DynamicTest> { data class TestCase( val text:

    String, val expect: Int ) return listOf<TestCase>( TestCase("test", 4), TestCase("cat", 3) ).map { case -> dynamicTest(case.toString()) { assertThat(case.text.length, equalTo(case.expect)) } } }
  9. package ${PACKAGE_NAME} import androidx.room.Room ~以下省略~ @RunWith(AndroidJUnit4::class) class ${NAME}Test { private

    lateinit var context: Context private lateinit var db: ${DBNAME} @Before fun setUp() { context = ApplicationProvider.getApplicationContext() db = Room.inMemoryDatabaseBuilder( context, ${DBNAME}::class.java).build() } @After @Throws(IOException::class) fun tearDown() { db.close() } } テンプレート例
  10. package ${PACKAGE_NAME} import androidx.room.Room ~以下省略~ @RunWith(AndroidJUnit4::class) class ${NAME}Test { private

    lateinit var context: Context private lateinit var db: ${DBNAME} @Before fun setUp() { context = ApplicationProvider.getApplicationContext() db = Room.inMemoryDatabaseBuilder( context, ${DBNAME}::class.java).build() } @After @Throws(IOException::class) fun tearDown() { db.close() } } 完全修飾名ではなく Import文を設定しておく
  11. package ${PACKAGE_NAME} import androidx.room.Room ~以下省略~ @RunWith(AndroidJUnit4::class) class ${NAME}Test { private

    lateinit var context: Context private lateinit var db: ${DBNAME} @Before fun setUp() { context = ApplicationProvider.getApplicationContext() db = Room.inMemoryDatabaseBuilder( context, ${DBNAME}::class.java).build() } @After @Throws(IOException::class) fun tearDown() { db.close() } } ファイル作成時に値を入力できる
  12. .assertNull : assertNull ANY → junit.framework.TestCase.assertNull($expr$) .assertNonNull : assertNotNull ANY

    → junit.framework.TestCase.assertNotNull($expr$) テンプレート例
  13. .assertNull : assertNull ANY → junit.framework.TestCase.assertNull($expr$) .assertNonNull : assertNotNull ANY

    → junit.framework.TestCase.assertNotNull($expr$) Postfix completionを呼び出す キーワード
  14. .assertNull : assertNull ANY → junit.framework.TestCase.assertNull($expr$) .assertNonNull : assertNotNull ANY

    → junit.framework.TestCase.assertNotNull($expr$) どの式で有効になるか KotlinはANYのみ
  15. .assertNull : assertNull ANY → junit.framework.TestCase.assertNull($expr$) .assertNonNull : assertNotNull ANY

    → junit.framework.TestCase.assertNotNull($expr$) ここにタイプ中の式がはいる
  16. .assertNull : assertNull ANY → junit.framework.TestCase.assertNull($expr$) .assertNonNull : assertNotNull ANY

    → junit.framework.TestCase.assertNotNull($expr$) 完全修飾名でimport文も追加されるが、 Kotlinのstatic importは非対応
  17. Sample.kt class Sample { val text = "Hi" fun hoge

    () { } } これをPsiで表現すると
  18. val editor = anActionEvent.getData(CommonDataKeys.EDITOR) val offset = editor.caretModel.offset val psiFile

    = anActionEvent.getData(CommonDataKeys.PSI_FILE) val element = psiFile.findElementAt(offset) val namedFunctionPsi = PsiTreeUtil.getParentOfType(element, KtNamedFunction::class.java) こんな感じ
  19. val editor = anActionEvent.getData(CommonDataKeys.EDITOR) val offset = editor.caretModel.offset val psiFile

    = anActionEvent.getData(CommonDataKeys.PSI_FILE) val element = psiFile.findElementAt(offset) val namedFunctionPsi = PsiTreeUtil.getParentOfType(element, KtNamedFunction::class.java) 現在のカーソル位置を取得
  20. val editor = anActionEvent.getData(CommonDataKeys.EDITOR) val offset = editor.caretModel.offset val psiFile

    = anActionEvent.getData(CommonDataKeys.PSI_FILE) val element = psiFile.findElementAt(offset) val namedFunctionPsi = PsiTreeUtil.getParentOfType(element, KtNamedFunction::class.java) カーソルがあたっているelementを取得
  21. val editor = anActionEvent.getData(CommonDataKeys.EDITOR) val offset = editor.caretModel.offset val psiFile

    = anActionEvent.getData(CommonDataKeys.PSI_FILE) val element = psiFile.findElementAt(offset) val namedFunctionPsi = PsiTreeUtil.getParentOfType(element, KtNamedFunction::class.java) カーソルがあたっているElementが 関数の一部だったらその関数の情報を取得
  22. Functionのパース KtNamedFunction name typeReference valueParamters Function名 戻り値の型 引数 ほしい containingClass

    クラス KtParameter型のリストで、 KtParameterはTypeRerefenceを持って いる
  23. 型情報の取得 val context: BindingContext = typeReference.analyze() val type: KotlinType? =

    context[BindingContext.TYPE, typeReference] KotlinType型 所属するpackage等、様々な情報を取得 できる
  24. @TestFactory fun length(): Collection<DynamicTest> { data class TestCase( val text:

    String, val expect: Int ) return listOf<TestCase>( TestCase("test", 4), TestCase("cat", 3) ).map { case -> dynamicTest(case.toString()) { assertThat(case.text.length, equalTo(case.expect)) } } } 作りたいコードの例
  25. テストメソッドの生成 val collection = ClassName("kotlin.collections", "Collection") val dynamicTest = ClassName("org.junit.jupiter.api",

    "DynamicTest") val testFunctionBuilder = FunSpec.builder("test${function.name.capitalize()}") .returns(collection.parameterizedBy(dynamicTest)) .addAnnotation(TestFactory::class.java) .addStatement("%L", testCaseClass.build()) .addStatement("%L", dynamicTestStatement.build())
  26. テストメソッドの生成 val collection = ClassName("kotlin.collections", "Collection") val dynamicTest = ClassName("org.junit.jupiter.api",

    "DynamicTest") val testFunctionBuilder = FunSpec.builder("test${function.name.capitalize()}") .returns(collection.parameterizedBy(dynamicTest)) .addAnnotation(TestFactory::class.java) .addStatement("%L", testCaseClass.build()) .addStatement("%L", dynamicTestStatement.build()) テストメソッド内に定義したいものを Builderパターンで追加していく
  27. テストコード自動生成を実現する道具 • Live Template • File Template • Postfix Completion

    • Parameterizedテスト生成Plugin Android Studioから カスタマイズ可能
  28. テストコード自動生成を実現する道具 • Live Template • File Template • Postfix Completion

    • Parameterizedテスト生成Plugin 自動生成の仕組みを紹介 実装を変更することで カスタマイズ可能