いいユニットテストってなに...?!
ユニットテストを考える
少し前に、テスト関連の記事として、以下を作成しました。
今現在、仕事の中でも自分の個人開発のプロジェクトでも、
ユニットテストを書いているのでこのタイミングで、、、、
"ユニットテストを書くことが目的になっていないか?"
ユニットテストのそもそもの目的だったり、大事なことを今回はまとめていきたいと思います!
ユニットテストとは
と言う最初の部分から入るが、簡単にいうならば、
プログラムを構成するちいさな単位(関数やメソッド,クラス,モジュールのこと =コンポーネント)が
個々の機能を正しく果たしているかどうかを検証するテストのこと、だ。
なんでユニットテストを書くの?
ユニットテストを書くのは、書くことが目的になってはいけない、
なんのために書くのかを認識したい。
"ソフトウェアの品質向上"のため、
と一言で言えるがそれをもう少し細かくすると以下のようになる。
1. バグを早期に発見するため
先にテストコードから書き始めると、
コードの小さな部分(メソッドなど)を個別にテストし、早い段階でバグを発見できる。
これにより、後から問題が発生するリスクを減らすことができる。
また、バグが本番環境に到達する前に検出することもでき、品質の担保になる。
2. ドキュメントとしての役割
コードがどのように機能するべきか?を示すドキュメントとしての役割も果たすことができる。
他の開発者がコードの意図を理解する助けにもなる。
3. リファクタリング後の信頼性を高めるため
コードをリファクタリングする際、ユニットテストがあれば、
変更が意図しない部分に影響を与えていないか確認できる。
これにより、コードのリファクタが安心して行うことが可能だ。
補足: ソフトウェアの品質って何?
明示された条件下で使用するとき,明示的ニーズ又は暗黙のニーズを満たすためのソフトウェア製品の能力。(ISO/IEC 25000:2014, JIS X 25000:2017 4.33項)
ソフトウェアの品質を測るために、ISO[2] 25010 では、以下の2つの品質モデルを定義している。
(正しくは2つを合わせて13 個の品質特性と 40 個の副特性が定義されている。)
- 利用者から見た利用時の品質(=外部品質特性): UIや使用合ってるのかetc
- 開発者から見た製品の品質(=内部品質特性)
外部品質特性
外部品質特性
機能適合性: ソフトウェアが期待される機能を提供し、指定された要件を満たしているかどうか。
性能効率: システムの性能が要求される効率を達成しているかどうか。
使用性: ソフトウェアがユーザにとってどれだけ使いやすいか。
信頼性: システムが障害なく動作し続ける能力。
セキュリティ: ソフトウェアがデータと情報を保護する能力。
互換性: 他のシステムや環境とどれだけ互換性があるか。
内部品質特性
内部品質特性
保守性: ソフトウェアが変更や改良をどれだけ容易に行えるか。
移植性: ソフトウェアが異なる環境でどれだけ容易に動作するか。
再利用性: コードやコンポーネントが再利用可能であるかどうか。
解析性: ソフトウェアの問題や欠陥をどれだけ簡単に識別できるか。
テスト性: ソフトウェアがどれだけテストしやすいか。
変更性: ソフトウェアの変更がどれだけ容易であるか。
いいユニットテストって何?
上記が、ユニットの目的だが、その目的を果たすようなユニットテストってどんなテストコード??
と言うことで、気をつけるべきこと共に書いていく。
1. シンプルで明確であること
テストは単一の機能やロジックにフォーカスし、
何をテストしているのかが、一目でわかるように書かれていることが大事だ。
複雑さを避け、テストの意図が明確であることが重要。
2. 再現性がある
テストはいつ、どの環境で実行しても同じ結果が得られるべきだ。
外部依存を排除し、純粋にテスト対象のコードのみを検証します。
ex.) LocalDateTime.now()
などの外部依存をそのまま使用する ❌
→ 固定値にする
3. 独立している
各テストケースは他のテストに依存せず、個別に実行できるよう設計されていることが大事。
注意点: ユニットテストはSOLID原則意識しなくても良い
ユニットテストでは、SOLID原則を必ずしも意識する必要はない。
これは私が初期に勘違いしていたことでもあるが、なんでもcompanion objectで作っておこう
としてしまうと、逆に可読性が落ちる場合もある。
簡単な場合(変数名で明確にすぐわかる場合)は変数にしてもいいが、
テストの大事なことは、
テストコードを読んだときに何がテストされているのか直感的に理解できることだ。
例えば、以下のようなケースでは定数化がかえって問題を引き起こす。
ex.
class UserRegisterServiceTest {
companion object {
const val VALID_EMAIL = "test@example.com"
const val VALID_NAME = "Test User"
const val VALID_PASSWORD = "securePassword123"
const val INVALID_EMAIL = ""
const val EXPECTED_ERROR_MESSAGE = "Validation error: All fields must be filled"
}
@Test
fun `test user registration with valid data`() {
val user = UserEntity(VALID_EMAIL, VALID_NAME, VALID_PASSWORD)
val service = UserRegistrationService()
val isRegistered = service.register(user)
assertTrue(isRegistered)
}
@Test
fun `test user registration fails with missing email`() {
val user = UserEntity(INVALID_EMAIL, VALID_NAME, VALID_PASSWORD)
val service = UserRegistrationService()
val exception = assertThrows<IllegalArgumentException> {
service.register(user)
}
assertEquals(EXPECTED_ERROR_MESSAGE, exception.message)
}
}
これだと直感的に理解しづらい。
シンプルさと明確さを重視して、直書きの方がいいのか?変数かしてもいいのか?
使い分けが大事!
おわり
他にも大事なことなどあったらコメントで書いてもらえてら嬉しいです!
まだまだなので引き続き頑張っていきたいと思います(><)
Discussion