テストコードが必要な理由とテストコードの間違った考え方

はじめに

今回のテーマ

・テストコードが必要な理由について

・テストコードのよくある間違った考え方


最近のソフトウェア開発の現場では

テストコードを書くのがあたり前になってきています。


しかし、

今までテストコードを書いてこなかった方や、

初めてソフトウェア開発の現場に入る方などは、


開発ルールでそういう決まりだからとか、

他のメンバーがやってるから

ただ他を真似してテストコードを書いている。

という方は少なくないのかなと思います。


今回はそのような方に向けた内容です。

動画にもまとめたので是非ご覧ください。


なぜテストコードが必要なのか??

ソフトウェア開発プロジェクト(プロダクト)の成長を持続可能にしたい

というのがテストコードを書くモチベーションの1つです。


プロダクトは競合他社との競争に勝つために、

機能の追加をものすごいスピードで求められます。



一方で、

コードの整理やリファクタリングなどの適切な処理が常にできていなければ、

とてつもないスピードで機能追加をしていくと、

ビジネスロジックがあちこちに散らばったり、

同じようなビジネスロジックがあちこちに書かれたり、

持つべき責務を超えた大きすぎるクラスができたりと・・・

無秩序なコードが増えていき、

その結果プロダクトの成長スピードは急激に落ちていきます。


では、

無秩序なコードの量を減らすために

コード整理やリファクタリングを行い続けるには何が必要でしょうか??


ここで、

テストコードが必要になります。


コードに変更を加えることで、

意図せず今まで動いていたコードが動かなくなる退行(リグレッション)を検出できる

セーフティネットがテストコードによって備わることで、

恐れることなくコードの整理やリファクタリングを常に行うことができるのです。


テストコードによって、

退行(リグレッション)が簡単に検出できるようになる。

そのため、無秩序の量を減らす活動を積極的に行え、

プロダクトの成長スピードを持続可能にできます。


テストコードに対するよくある間違った考え方

プロダクトの成長スピードを持続可能にするためには

テストコードは欠かせません。

しかし、テストコードの質が低ければ、

このようなことに陥ります。

  • 成功すべきなのに失敗する
  • 失敗すべきなのに成功する
  • 実行時間がかかりすぎる
  • 保守コストが高すぎる


プロダクションコードと同じく、

テストコードも負債の面があり保守が必要で、

バグに対して脆弱だということを忘れてはいけません。


では、テストコードに対するよくある間違った考え方について

具体例をいくつか見ていきましょう。


網羅率を上げるのが質の高いテストコードではない

プロダクションコードのビジネス的に重要だったり、

特に複雑なアルゴリズムの箇所がある一方で、

そこまで複雑ではないシンプルな箇所もあります。


コード網羅率だけを意識すると、

ビジネス的に重要だろうと

シンプルな箇所だろうと

同じようにテストすることになります。


テストコードもプロダクションコードと同じで、

負債を抱え、保守が必要ということを考えると、

コスパ悪くテストコードの保守コストだけが高くなる。

ということは避けるべきなので、

ビジネス的に重要・複雑なアルゴリズムの箇所には重点的に

テストコードを書き、

そうでない箇所には書きません。

(書かなくてもいいではなく、書いてはダメ)


❌価値のないテストは書かない
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)
    }

}


これは極端な例ですが、

ボイラープレートやgetter、setterを持つデータクラス

なども同じようにテストコードを

書くべきではないと僕は考えています。


⭕️ビジネス的に重要な部分にテストコードを書く
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)
        }

    }
}


送料算出というビジネス的に重要な部分には

テストコードを重点的に書きましょう。

これくらいシンプルならテストコードを書かない。

という方もおられるかもしれませんが、

ここはバグを埋め込むと被害が大きいので

シンプルだとしても念入りに僕は書きます。


プロダクションコードを見た上で、

「まだまだ取るに足らないコードだから、

コスパ考えると今はテストコードを書く必要はないな。」

という判断も時には重要で、

テストコードを書かないこともテストコードの質を上げる

(というか下げない)ためないためには必要なのです。


網羅率ではテストコードの質は測定できない!!

コード網羅率だけでなく、分岐網羅率を利用したとしても

テストコードの質は計測できません。


例えば、

リファクタリングでコード記述量を減らせた場合、

テストコードの質が悪くても網羅率は上がり、


テストコードで検証(assertionで結果の付き合わせ)が行われなくても、

テスト対象のコードが多く実行されるだけで網羅率が上がるからです。


網羅率を上げることだけを目的にすると、

テストコードの保守コストや負債だけが上がり、

テストコードがもたらす価値はなかなか上がらない。

という事態に陥るので注意が必要です。


テストコードを過度に複雑にしてテストするのが質の高いテストコードではない

テストコードはテスト対象を使う側になります。

テスト対象が悪い設計をしていれば、テスト対象の使い勝手は悪く、

テストコードもそれに引きずられます。


その際、

テストコードを複雑にしてでもテスト対象を検証しようとすると、

テストコードの保守コストが上がるだけではなく、

根本的なテスト対象の悪い設計の改善にはなりません。


テストコードが複雑になってきたら、

テスト対象の設計見直すべき場合もあります。


価値のあるテストコードはテストコードだけでは作れないのです。

設計テクニックを知った上で、

テスト対象の設計の良し悪しを分析し、

テスト対象とテストコードの両方から

テストコードの質を上げる必要があるのです。


今回のテーマについては以上です。

テストコード初めての方へ向けて記事を書いたので

是非こちらもご覧ください。

初めてテストコードを書く方に向けて|新人プログラマー時代の自分に伝えたいこと


参考文献

第1章 なぜ、単体(unit)テストを行うのか?

単体テストの考え方/使い方

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

このサイトはスパムを低減するために Akismet を使っています。コメントデータの処理方法の詳細はこちらをご覧ください