クラスを再帰的に評価する usingRecursiveComparison()
こんにちは。株式会社 Interfamilia の Waka です。
早いもので、もう9月になりました。まだまだ暑さが続きますね
先月は自分の身の回りで体調を崩す方が続出していました。
皆さんも熱中症には十分お気を付けください🥺
今回の記事では、AssertJに関するTIPSをご紹介したいと思います!
目次
AssertJは、JVM系言語向けのアサーションライブラリです。
テスト結果を検証する時に便利なメソッドが沢山用意されています。
中でも usingRecursiveComparison()
は非常に便利なメソッドです。
AssertJ 3.12.0 で追加されたメソッドで、 入れ子になってるクラスの各要素1を再帰的に評価できます。
実際の使用例を交えて、紹介したいと思います。
kotlin: 1.9.10
assertj-core: 3.20.2
以下のMonster
クラス同士を比較する場合を考えてみます。
data class Monster(
val id: Long,
val name: String,
val gender: Gender,
val skill: Skill,
)
enum class Gender { MALE, FEMALE, NONE }
data class Skill(
val skill1: String,
val skill2: String? = null,
)
2つのMonster型 (monster1, monster2) を usingRecursiveComparison()
で比較する時の記述例です。
メソッドチェーンで isEqualTo()
を繋ぎ、2つのMonster型を比較しています
プロパティ値が全て等しいので、テストが成功します。
@Test
fun test() {
val monster1 = Monster(
id = 445,
name = "ガブリアス",
gender = Gender.MALE,
skill = Skill(
skill1 = "ドラゴンダイブ",
),
)
val monster2 = Monster(
id = 445,
name = "ガブリアス",
gender = Gender.MALE,
skill = Skill(
skill1 = "ドラゴンダイブ",
),
)
assertThat(monster1)
.usingRecursiveComparison()
.isEqualTo(monster2)
}
以下の例は gender
,skill
のプロパティ値が異なるので、テストが失敗します。
@Test
fun failTest() {
val monster1 = Monster(
id = 445,
name = "ガブリアス",
gender = Gender.MALE,
skill = Skill(
skill1 = "ドラゴンダイブ",
skill2 = null,
),
)
val monster2 = Monster(
id = 445,
name = "ガブリアス",
gender = Gender.FEMALE, // 差分
skill = Skill(
skill1 = "ドラゴンダイブ",
skill2 = "じしん", // 差分
),
)
assertThat(monster1)
.usingRecursiveComparison()
.isEqualTo(monster2)
}
テスト失敗時、ログに表示されるエラーメッセージは以下になります。
一致しなかった箇所だけがピックアップされるので、特定が容易です。
Expecting actual:
Monster(id=445, name=ガブリアス, gender=MALE, skill=Skill(skill1=ドラゴンダイブ, skill2=null))
to be equal to:
Monster(id=445, name=ガブリアス, gender=FEMALE, skill=Skill(skill1=ドラゴンダイブ, skill2=じしん))
when recursively comparing field by field, but found the following 2 differences:
field/property 'gender' differ:
- actual value : MALE
- expected value: FEMALE
field/property 'skill.skill2' differ:
- actual value : null
- expected value: "じしん"
The recursive comparison was performed with this configuration:
- no overridden equals methods were used in the comparison (except for java types)
- these types were compared with the following comparators:
- java.lang.Double -> DoubleComparator[precision=1.0E-15]
- java.lang.Float -> FloatComparator[precision=1.0E-6]
- java.nio.file.Path -> lexicographic comparator (Path natural order)
- actual and expected objects and their fields were compared field by field recursively even if they were not of the same type, this allows for example to compare a Person to a PersonDto (call strictTypeChecking(true) to change that behavior).
- the introspection strategy used was: DefaultRecursiveComparisonIntrospectionStrategy
ちなみに、AssertJ標準のassertEquals()
でも同様の比較 (クラス内の要素を再帰的に評価) は可能です。
ただし、プロパティ値が一致しなかった時のエラーメッセージが、非常に分かりにくくなっています。
以下が assertEquals()
で比較した場合のエラーメッセージ全文です。
expected: <Monster(id=445, name=ガブリアス, gender=FEMALE, skill=Skill(skill1=ドラゴンダイブ, skill2=じしん))> but was: <Monster(id=445, name=ガブリアス, gender=MALE, skill=Skill(skill1=ドラゴンダイブ, skill2=null))>
予想:Monster(id=445, name=ガブリアス, gender=FEMALE, skill=Skill(skill1=ドラゴンダイブ, skill2=じしん))
実際:Monster(id=445, name=ガブリアス, gender=MALE, skill=Skill(skill1=ドラゴンダイブ, skill2=null))
こうしてみると、テスト結果と期待値の等値比較が、たった数行のコードで実現できる usingRecursiveComparison()
の方が便利に感じられます。
ignoringFields()
をメソッドチェーンで繋ぐと、比較時に無視したい要素をスキップできます。
ID、タイムスタンプ等、固有のユニークな値を無視して、それ以外の要素を等値比較したい時に便利です。
以下の例は ignoringFields()
を使用して id
,gender
,skill
以外の要素を比較しています。
それ以外のプロパティ値が等しいので、テストが成功します。
@Test
fun ignoringFieldTest() {
val monster1 = Monster(
id = 132,
name = "ガブリアス",
gender = Gender.NONE,
skill = Skill(
skill1 = "へんしん",
),
)
val monster2 = Monster(
id = 445,
name = "ガブリアス",
gender = Gender.MALE,
skill = Skill(
skill1 = "ドラゴンダイブ",
skill2 = "じしん",
),
)
assertThat(monster1)
.usingRecursiveComparison()
.ignoringFields("id", "gender", "skill")
.isEqualTo(monster2)
}
上の使用例を全て纏めたサンプルコードです。
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Test
import kotlin.test.assertEquals
class UsingRecursiveComparisonTest {
// テストが成功するケース (usingRecursiveComparison)
@Test
fun test() {
val monster1 = Monster(
id = 445,
name = "ガブリアス",
gender = Gender.MALE,
skill = Skill(
skill1 = "ドラゴンダイブ",
),
)
val monster2 = Monster(
id = 445,
name = "ガブリアス",
gender = Gender.MALE,
skill = Skill(
skill1 = "ドラゴンダイブ",
),
)
assertThat(monster1)
.usingRecursiveComparison()
.isEqualTo(monster2)
}
// テストが成功するケース (assertEquals)
@Test
fun test() {
val monster1 = Monster(
id = 445,
name = "ガブリアス",
gender = Gender.MALE,
skill = Skill(
skill1 = "ドラゴンダイブ",
),
)
val monster2 = Monster(
id = 445,
name = "ガブリアス",
gender = Gender.MALE,
skill = Skill(
skill1 = "ドラゴンダイブ",
),
)
assertEquals(monster2, monster1)
}
// テストが失敗するケース (usingRecursiveComparison)
@Test
fun failTest() {
val monster1 = Monster(
id = 445,
name = "ガブリアス",
gender = Gender.MALE,
skill = Skill(
skill1 = "ドラゴンダイブ",
skill2 = null,
),
)
val monster2 = Monster(
id = 445,
name = "ガブリアス",
gender = Gender.FEMALE, // 差分
skill = Skill(
skill1 = "ドラゴンダイブ",
skill2 = "じしん", // 差分
),
)
assertThat(monster1)
.usingRecursiveComparison()
.isEqualTo(monster2)
}
// テストが失敗するケース (assertEquals)
@Test
fun failTestAssertEquals() {
val monster1 = Monster(
id = 445,
name = "ガブリアス",
gender = Gender.MALE,
skill = Skill(
skill1 = "ドラゴンダイブ",
skill2 = null,
),
)
val monster2 = Monster(
id = 445,
name = "ガブリアス",
gender = Gender.FEMALE, // 差分
skill = Skill(
skill1 = "ドラゴンダイブ",
skill2 = "じしん", // 差分
),
)
assertEquals(monster2, monster1)
}
// テストが成功するケース (ignoringFieldsで一部プロパティを無視)
@Test
fun ignoringFieldTest() {
val monster1 = Monster(
id = 132,
name = "ガブリアス",
gender = Gender.NONE,
skill = Skill(
skill1 = "へんしん",
),
)
val monster2 = Monster(
id = 445,
name = "ガブリアス",
gender = Gender.MALE,
skill = Skill(
skill1 = "ドラゴンダイブ",
skill2 = "じしん",
),
)
assertThat(monster1)
.usingRecursiveComparison()
.ignoringFields("id", "gender", "skill")
.isEqualTo(monster2)
}
}
data class Monster(
val id: Long,
val name: String,
val gender: Gender,
val skill: Skill,
)
enum class Gender { MALE, FEMALE, NONE }
data class Skill(
val skill1: String,
val skill2: String? = null,
)
以上、AssertJの便利なメソッド usingRecursiveComparison()
の紹介でした。
テストを書く時に重宝しているのですが、意外と知名度が低く、ネット上に情報が少ないように感じたので、今回改めて記事にした次第です。
他にも色々と便利なオプションがあるので、公式ドキュメント もご確認いただければと思います。
それではまた次回!
- RecursiveComparisonAssert (AssertJ fluent assertions 3.12.0 API)
- AssertJ - fluent assertions java library | Field by field recursive comparison
- AssertJでよく使うテスト API - Qiita
- AssertJ 3.12.0リリースされたので新機能試してみた - BullよりElk
応募フォームには、外部サービスengageを利用しています。
ご応募に際して、何かわからないことなどございましたら こちら
からお気軽にお問い合わせください。
Kotlinにおけるプロパティ、Javaにおけるフィールドを指します。 ↩︎