🚀

Kotlin版Power Assertツール「Power-assert」の紹介

2024/06/24に公開

はじめに

株式会社ログラスでエンジニアをしております、中村です。
既存のアサーションライブラリを使ったテストの書き方には癖があり、検証対象のパターンごとに適切な関数を使い分ける必要があります。世の中にはアサーションライブラリの使い方だけにフォーカスした記事も数多く存在し、これらの記事にお世話になった方も多いのではないでしょうか。かく言う私もお世話になったエンジニアのうちの一人で、テストとは直接関係のないところにコストをかけていて「不毛だな〜」と感じつつも、せっせとアサーションを書いていました。
そこで、そんな悩みを一気に解決するKotlin向けPower Assertツールを紹介します。

Power-assertとは

Power-assert compiler plugin

Power-assertは、Power Assert機能を提供するKotlinコンパイラープラグインです。デバッグ情報を視覚的に表示させることで、アサーションの問題箇所を直感的に理解できるようになります。

Power-assert(Power Assert系ライブラリ)の特徴

特別な工夫なくアサーションのデバッグ情報をコードレベルで視覚的に表示します。

@Test fun test() {
    val five = 5
    val fifteen = 10
    val twelve = 20
    assert(five + fifteen == twelve)
}

実行結果:

Power-assertなし

Assertion failed
java.lang.AssertionError: Assertion failed
  at org.example.AppTest...

Power-assertあり

Assertion failed
assert(five + fifteen == twelve)
       |    | |       |  |
       |    | |       |  20
       |    | |       false
       |    | 10
       |    15
       5

デバッグ情報として、式の評価のプロセスで生成される中間値がコードの各部に視覚的に紐付けられています。five + fifteentwelveと等価ではないことだけではなく、fiveが5、fifteenが10、five + fifteenの結果が15であることが一目瞭然で、どこが間違えているのか即座に理解できます。

インストールと設定

Gradleでのインストール方法のみ紹介します。

build.gradle.kts

plugins {
    kotlin("jvm") version "2.0.0"
    // kotlin("multiplatform") version "2.0.0". // Kotlin Multiplatformでも動作します
    kotlin("plugin.power-assert") version "2.0.0" // Power-assertプラグインを追加
}

@OptIn(ExperimentalKotlinGradlePluginApi::class)
powerAssert {
    functions = listOf( // デバッグ情報を表示させたい関数・メソッド
        "kotlin.assert",
        "kotlin.require",
        "kotlin.test.assertTrue",
        "kotlin.test.assertEquals",
        "kotlin.test.assertNull",
    )
    includedSourceSets = listOf("test") // デバッグ情報を表示させたいソースセット
}

Power-assertプラグインをインストールすると、プロジェクトにpowerAssertエクステンションが作成されます。
プラグインをプロジェクトに追加するだけでPower Assert機能を使うことができますが、必要であればpowerAssertの設定を変更してください。

functions

デバッグ情報を表示させたい関数・メソッドのパス(fully-qualified function path)を指定します。デフォルトでkotlin.assertのみ指定されています。
コンパニオンオブジェクトに属するメソッドはXxx.Companion.yyyで指定できます。

includedSourceSets

デバッグ情報を表示させたいソースセットを指定します。

includedSourceSets = listOf("main", "test")とすれば、プロダクションコードでもデバッグ情報を表示させることができます。

使用例

コレクション

コレクションと取り出した要素も表示されます。

@Test fun test () {
    val names = listOf("Alice", "Bob", "Carol")
    val index = 2
    val expeced = "Alice"
    assert(names[index] == expeced)
}

assert(names[index] == expeced)
       |     |      |  |
       |     |      |  Alice
       |     |      false
       |     Carol
       |     2
       [Alice, Bob, Carol]

オブジェクト

toString()の結果が表示されます。

data class Vector3(val x: Float, val y: Float, val z: Float)

@Test fun test() {
    val vector1 = Vector3(1.1f, -2.3f, 40.22f)
    val vector2 = Vector3(20.9f, -3.0f, -82.1f)
    assert(vector1 == vector2)
}

Assertion failed
assert(vector1 == vector2)
       |       |  |
       |       |  Vector3(x=20.9, y=-3.0, z=-82.1)
       |       false
       Vector3(x=1.1, y=-2.3, z=40.22)

メソッドやメンバ変数へのアクセス

メンバやメソッドの戻り値も表示されます。

@Test fun test() {
    val id = "半角英数字のみ、32文字未満だよ"
    val regex = Regex("""[0-9A-Za-z]""")
    assert(id.length < 32 && id.matches(regex))
}

assert(id.length < 32 && id.matches(regex))
       |  |      |       |  |       |
       |  |      |       |  |       [0-9A-Za-z]
       |  |      |       |  false
       |  |      |       半角英数字のみ、32文字未満だよ
       |  |      true
       |  16
       半角英数字のみ、32文字未満だよ

改行されたアサーション

アサーションコードが改行されていても表示が崩れることはありません。

fun main() {
    val year = 2100
    assert(year % 4 == 0
        && (
            year % 100 != 0 ||
            year % 400 == 0
        )
    )
}

Assertion failed
assert(year % 4 == 0
       |    |   |
       |    |   true
       |    0
       2100
    && (
        year % 100 != 0 ||
        |    |     |
        |    |     false
        |    0
        2100
        year % 400 == 0
        |    |     |
        |    |     false
        |    100
        2100
    )
)

自作のアサーション関数

powerAssert.functionsにパスを追加することで自作の関数・メソッドにもデバッグ情報を表示させることができます。
ただし、デバッグ情報が表示できる関数には条件があります。

// build.gradle.kts

@OptIn(ExperimentalKotlinGradlePluginApi::class)
powerAssert {
    functions = listOf(
        "kotlin.assert",
        "kotlin.require",
        "kotlin.test.assertTrue",
        "kotlin.test.assertEquals",
        "kotlin.test.assertNull",
        "kotlin.test.assertNotNull",
        "org.example.myAssert",
    )
    includedSourceSets = listOf("test", "main")
}

package org.example

fun myAssert(value1: String, value2: Boolean, value3: Float, lazyMessage: () -> Any = { "Assertion failed" }) {
    throw IllegalArgumentException(lazyMessage().toString())
}

@Test fun test() {
    val name = "Raika"
    myAssert(
        "Hello, $name!",
        4.8 - 4.7 - 0.1 == 0.0,
        Math.PI
    )
}

myAssert(
    "Hello, $name!",
    |        |
    |        Raika
    Hello, Raika!
    4.8 - 4.7 - 0.1 == 0.0,
        |     |     |
        |     |     false
        |     -3.608224830031759E-16
        0.09999999999999964
    Math.PI,
         |
         3.141592653589793
)

メリットと注意点

メリット

デバッグと複雑な条件式の確認

Power-assertは具体的な値や計算結果を視覚的に表示するため、複雑な条件式でも何が間違っているのか一目で理解できます。例えば、次のような複数の要素が絡まる条件のアサーションでも、どの部分が原因で失敗しているのか簡単に特定することが可能です。

fun main() {
    val a = 5
    val b = 10
    val c = 15
    val map = mapOf("key1" to "value1", "key2" to "value2")
    assert(a + b == c && map["key1"] == "value3")
}

Assertion failed
assert(a + b == c && map["key1"] == "value3")
       | | | |  |    |           |
       | | | |  |    |           false
       | | | |  |    value1
       | | | |  |    {key1=value1, key2=value2}
       | | | |  15
       | | | true
       | | 10
       | 15
       5

アサーションライブラリに依存せず、直感的にテストが書ける

JUnitやAssertJのようなアサーションライブラリは、失敗したアサーションのデバッグ情報を提供する強力なツールである一方、以下のデメリットがあります。

  • アサーションの書き方はアサーションライブラリに大きく依存する
  • アサーションライブラリで対応していないパターンのデバッグ情報が見たい場合、カスタマイズする必要がある

Power-assertであればどんなアサーションでも自動的にデバッグ情報が表示されるため、デバッグ情報を表示するためのアサーションライブラリが必要なくなります。アサーションライブラリの学習コストがなくなる上、純粋なKotlinコードだけで完結するので、プロダクションコードの感覚でテストが書けるようになります。

import org.assertj.core.api.Assertions.assertThat

fun main() {
    val a = 5
    val b = 10
    assertThat(a + b).isEqualTo(20)
}

fun main() {
    val a = 5
    val b = 10
    assert(a + b == 20)
}

注意点

実験版

Power-assertは記事公開時点(2024年6月24日)で実験版であり、機能の安定性や互換性に関しては注意が必要です。
使用する際は最新の情報を確認し、必要に応じてバックアップやバージョン管理を行ってください。

デバック情報が表示される関数の条件

Power-assertによるデバッグ情報はすべての関数・メソッドで表示されるわけではありません。
以下の場合にのみデバッグ情報が表示されます。

  • 最後の引数の型
    • String
    • String型のスーパータイプ
    • 上二つのどちらかを戻り値とし、引数は取らないラムダ

おわりに

Kotlinでのデバッグ体験を大幅に改善する非常に強力なツールであるPower-assertの紹介でした。
多くの方がアサーションライブラリ独特の書き方に苦労したことがあると思うので、サクッとPower-assertを導入して生産性を上げていきましょう!

株式会社ログラス テックブログ

Discussion