はじめに
今回は
- テスト対象を分析して4つに分類する方法
- テスト対象の分類に応じてやるべきこと
についてまとめました。
テストコードの価値を高めるためには、
テスト対象を分析し、
その分析結果に応じてリファクタリングや
テストコードの削除・改善などを適切に行う必要があります。
動画にもまとめたのでご覧ください。
テスト対象を4つに分類する
テスト対象は2つの視点で分類できます。
- コードの複雑さとドメインにおける重要性
- 協力者オブジェクト(依存)の数
コードの複雑さは、
テスト対象の分岐の数(判断を行う箇所)の数で計測でき、
ドメインのにおける重要性は、
ビジネスの問題領域におけるテスト対象の重要性で決まります。
このような箇所には、
重点的に単体テストを書く必要があります。
逆にコードがシンプルでドメインにおける重要性も低い箇所に
テストコードを形だけ書いている場合、
その単体テストに価値はあまりありません。
一方で、
テスト対象の協力者オブジェクト(依存)が多い場合、
テストコードはテストの準備で、依存をモックしたり、
テストコードが長く・複雑になるため
保守しずらいテストコードになります。
テスト対象を
これら2つの観点で4つに分類するとこのようになります。
それでは、
4つに分類されたテスト対象の特徴と、
それぞれに対しての適切な対処についてそれぞれ見ていきます。
ドメイン・モデル/アルゴリズムに分類されるテスト対象
ビジネス的に重要だったり、
ビジネスとは直接関係ないが複雑だったりするロジックです。
そのため、特に退行に対する保護を強く備えておく必要があります。
class NormalRankDelivery: Delivery {
override fun getPostage(purchaseAmount: Int): Int {
return if (purchaseAmount >= 3_000) 0 else 100
}
override fun canTodayDelivery(): Boolean {
return false
}
}
class TestNormalRankDelivery {
@Nested
inner class GetPostage {
@Test
fun `購入金額が3000円未満のため送料は100円`() {
val normalRankDelivery = NormalRankDelivery()
val actual = normalRankDelivery.getPostage(2999)
Assertions.assertEquals(100, actual)
}
@Test
fun `購入金額が3000円以上のため送料は無料`() {
val normalRankDelivery = NormalRankDelivery()
val actual = normalRankDelivery.getPostage(3000)
Assertions.assertEquals(0, actual)
}
}
}
テスト対象として実行されるコードが複雑や重要なロジックであるため、
テストコードでの検証により退行に対する保護をより強く備えられます。
また、
テスト対象となるコードに協力者オブジェクトが含まれることも少なく、
テスト実行のためにモックなどの準備が不要で、
シンプルな単体テストコードになり、
テストコードの保守コストが低く抑えられ、
迅速なフィードバックも得られます。
この分類へのテストは非常にコスパがいいです。
コントローラーに分類されるテスト対象
このコード自体は複雑でビジネス的に重要なことは行わず、
ドメインモデル、外部システム連携、データベースアクセス
などを行う複数のコンポーネントを連携させて、
1つのビジネス的なユースケースを実現します。
ここに分類されるテスト対象には、
統合テストを書きます。
@Test
fun `支払い登録できる`() {
every { memberRepository.findBy(any()) } returns MotherPaymentMember.default()
every { paymentRepository.save(any()) } returns Unit
every { distributeCouponRepository.findBy(any()) } returns null
target.execute(param())
verifySequence {
memberRepository.findBy(MemberCode("1"))
paymentRepository.save(any())
distributeCouponRepository.findBy(MemberCode("1"))
}
}
取るに足らないコードに分類されるテスト対象
setterやgetter、ガード節などのないコンストラクタなど、
複雑さがなく、ビジネス的に重要でもないような箇所です。
class Size(val width: Int, val height: Int, val depth: Int)
class TestSize {
@Test
fun `こんなテストは必要ない`() {
val width = 100
val height = 100
val depth = 100
val size = Size(width = width, height = height, depth = depth)
Assertions.assertEquals(width, size.width)
Assertions.assertEquals(height, size.height)
Assertions.assertEquals(depth, size.depth)
}
}
もしそのような箇所へのテストコードがあっても
保守コストがかかりテストに価値はないので削除します。
僕の基本方針としては、
ドメイン層だとしてもメチャクチャシンプルな
値オブジェクトやエンティティ
(ビジネス的には重要と分類され、複雑さはほぼゼロ)
へのテストもシンプルなうちは書かない。
という方針で良いと考えています。
過度に複雑なコードに分類されるテスト対象
コントローラーとして
データベース連携、外部システム連携などを行うのに加えて
ドメインロジックなどのビジネス的に重要で複雑なロジックも
持つようなコードです。
ここに分類されたテスト対象に対して、
そのままテストコードを書こうとしてはいけません。
もしここに分類されたら、
テスト対象の設計を見直し、
ドメイン・モデル/アルゴリズムとコントローラーに
分解するリファクタリングを行います。
過度に複雑なコードは責務を2つ持っているので、
作りが歪で、テストもしずらいです。
過度に複雑なコードは、
テストコードだけでなんとかするアプローチだと、
なんの解決にもならず、
テストコードの保守コストが跳ね上がるので注意が必要です。
今回のテーマについては以上です。
テストコード初めての方へ向けて記事を書いたので
是非こちらもご覧ください。
初めてテストコードを書く方に向けて|新人プログラマー時代の自分に伝えたいこと参考文献
第7章 単体テストの価値を高めるリファクタリング
単体テストの考え方/使い方