循環的複雑度はどのくらいに設定するべきか

ソースコードの複雑性を表す基準として、循環的複雑度という指標があります。
この循環的複雑度はどれくらいの設定値にするのが適切なのかをまとめておこうと思います。

循環的複雑度とは

循環的複雑度はThomas J. McCabe氏によって1976年に考案された指標です。
この値はソースコードの複雑性を示すものであり、この値を測定し、低い値を保つことで、ソースコードの保守性を高い状態で維持することができます。

循環的複雑度の目安

循環的複雑度の基準は、対象としているソフトウェアの複雑性や、ドメインの複雑性などに左右されます。
そのため、絶対の数値というものはありませんが、書籍などで紹介されているものの一部を見ていこうと思います。

MathWorks

MathWorksが公開している基準です。
循環的複雑度の指標としては一番引用される基準だと思います。

循環的複雑度 複雑さの状態 バグ混入確率
10以下 非常に良い構造 25%
30以上 構造的なリスクあり 40%
50以上 テスト不可能 70%
75以上 いかなる変更も誤修正を生む 98%

jp.mathworks.com

Software Architecture Metrics

ソースコードアーキテクチャなどを評価する様々なメトリクスを紹介している書籍です。
日本語訳は現時点で出版されていません。

This metric is well researched, and we know that error rates increase quickly for all values above 24. I recommend a threshold of 15, to stay on the safe side.

 ↓訳

この指標はよく研究されており、24を超えるすべての値に対してエラー率が急速に増加することがわかっています。安全を確保するために、私は15の閾値を推奨します。

Software Architecture Metrics: Case Studies to Improve the Quality of Your Architecture

ソフトウェアアーキテクチャの基礎

色々なソフトウェアアーキテクチャなどを紹介している書籍です。
原文は英語ですが、日本語訳版も出版されています。

In general, the industry thresholds for CC suggest that a value under 10 is acceptable, barring other considerations such as complex domains. We consider that threshold very high and would prefer code to fall under five, indicating cohesive, well-factored code. 

↓訳(日本語訳版を持っていなかったため原文から引用します)

一般的に、業界でのCC(循環的複雑度)の閾値は、複雑な領域などの他の考慮事項を除けば、10未満であれば受け入れられるとされています。私たちはその閾値を非常に高く考えており、5未満であれば、凝縮性のある、適切にファクタリングされたコードを示していると考え、好ましいと考えています。

ソフトウェアアーキテクチャの基礎 ―エンジニアリングに基づく体系的アプローチ

実際のソースコード

では、実施にJavaソースコードで、循環的複雑度が「5」のコード、「10」のコード、「15」のコードのサンプルを見てみたいと思います。
ソースコードの作成はChatGPTにお願いしていますが、CheckStyleでも確認しています。

循環的複雑度が5のコード

public class CyclomaticComplexityExample5 {
    public static void exampleMethod(int a, int b, int c, int d) {
        if (a > b) {
            // パス1
            System.out.println("a is greater than b");
        } else {
            // パス2
            System.out.println("b is greater than or equal to a");
        }
        for (int i = 0; i < c; i++) {
            // パス3
            System.out.println("i: " + i);
        }
        if (c > 10) {
            // パス4
            System.out.println("c is greater than 10");
        }
        if (d % 2 == 0) {
            // パス5
            System.out.println("d is even");
        }
    }
}

循環的複雑度が10のコード

public class CyclomaticComplexityExample10 {
    public static void exampleMethod(int a, int b, int c, int d, int e) {
        if (a > 0) { // 1
            System.out.println("a is positive");
        }
        if (b > 0) { // 2
            System.out.println("b is positive");
        }
        if (c > 0) { // 3
            System.out.println("c is positive");
        }
        if (d > 0) { // 4
            System.out.println("d is positive");
        }
        if (a < 0) { // 5
            System.out.println("a is negative");
        }
        if (b < 0) { // 6
            System.out.println("b is negative");
        }
        if (c < 0) { // 7
            System.out.println("c is negative");
        }
        if (d < 0) { // 8
            System.out.println("d is negative");
        }
        if (e != 0) { // 9
            System.out.println("e is non-zero");
        }
        // デフォルトパス(10)
        System.out.println("Check complete");
    }
}

循環的複雑度が15のコード

public class CyclomaticComplexityExample15 {
    public static void exampleMethod(int a, int b, int c, int d, int e) {
        if (a > 0) { // 1
            System.out.println("a is positive");
        }
        if (b > 0) { // 2
            System.out.println("b is positive");
        }
        if (c > 0) { // 3
            System.out.println("c is positive");
        }
        if (d > 0) { // 4
            System.out.println("d is positive");
        }
        if (e > 0) { // 5
            System.out.println("e is positive");
        }
        if (a < 0) { // 6
            System.out.println("a is negative");
        }
        if (b < 0) { // 7
            System.out.println("b is negative");
        }
        if (c < 0) { // 8
            System.out.println("c is negative");
        }
        if (d < 0) { // 9
            System.out.println("d is negative");
        }
        if (e < 0) { // 10
            System.out.println("e is negative");
        }
        if (a == 0) { // 11
            System.out.println("a is zero");
        }
        if (b == 0) { // 12
            System.out.println("b is zero");
        }
        if (c == 0) { // 13
            System.out.println("c is zero");
        }
        if (d == 0) { // 14
            System.out.println("d is zero");
        }
        // デフォルトパス(15)
        System.out.println("Evaluation complete");
    }
}

このサンプルコードは処理がすごく簡単なものですが、循環的複雑度が「15」のコードは条件分岐の量がそれなりに多く、しっかり処理がある場合にはそれなりの複雑なコードになりそうです。

まとめ

循環的複雑度の目安について見てみました。

基本的には「10」、もしくは「15」以下に設定するのが良さそうです。
それよりも更に厳しめに設定したい場合は、「5」や、5と10の中間の「8」などに設定するのが良いのではないでしょうか?
ただし、これらの設定値はソフトウェアの複雑性などのより一概には決められませんので、新規開発の際は最初は厳し目の値にしておいて、その基準を守るのがコストや保守性に見合わないと判断した場合に基準値を緩くしていくのが良いと思います。

ソフトウェア品質のテストの数について

ソフトウェアの品質を上げる、テストを改善するというと、今までよりもテストの量が増え、大変になるという風に捉えられることがあります。

そういう場合もあるかもしれませんが、必ずしもそうならないこともありますので、少しまとめておこうと思います。

テストの量≠ソフトウェアの品質向上

ソフトウェアの品質向上が重要なので、テストの量を増やすというアプローチはよく聞きます。
ただし、テストの量でテストの質を評価するというのは、ソースコードのstep数で開発能力や機能の難易度を測定するようなものと近いです。

ソースコードの規模で能力や難易度を図るというアプローチを行っているところもまだあるかもしれませんが、昨今はだんだん減っているのではないでしょうか?
会社のルールになっているから仕方なくソースコードの量で測定している、というところもまだあるかもしれません。

テストケースの数などは簡単に測定できるので単純に概要を知りたい場合は有効かもしれません。
ただし、それでテスト自体の本質が分かると思ってしまったり、テストケースの数を増やすことで品質を上げられる、品質を上げるためにはテストを頑張る、という施策を取ってしまうと、作業量は増えて大変になるけれども、本質的には何も改善しないということになり得ます。

アジャイル、DevOps時代の品質

アジャイルやDevOpsの考え方の中では、顧客への提供スピードを早く行い、仮設検証を早く行える仕組みを取り入れていきます。

その仮説検証の結果から更に仮説検証を行うという、仮説検証のフィードバックループを早く行えることが重要となります。

ソフトウェアの品質というと良く挙げられるものにISO/IEC 25000シリーズがあります。
これらの中では、顧客への提供スピードに近い項目というものはありません。

ただし、最近はDORA(Google)の中で、DevOpsの品質測定の基準というものが言及されています。
そのため、ソフトウェアの更新スピード、更新頻度などの価値提供のスピードは品質の一つと捉えられることも多くなってきているかと思います。

cloud.google.com

imtnd.hatenablog.com

ただし、提供スピードが早ければ良いというものでもありません。

個人開発や、企業の最初のプロダクトとして提供する場合は、テストはあまりせず、プロトタイプに近い形でユーザーに提供するということもあるかもしれません。

ただし、一定規模以上の企業の場合、実装されている機能が想定通りに動かなかったり、可用性が低いというような場合は、企業のブランドのイメージダウンに繋がります。
企業ブランドのイメージダウンというのは測定が難しい指標だと思いますが、気が付かないうちにダウンしているということになり得ます。
特にSNSで口コミが広がりやすい昨今ですので、一人のネガティブイメージが拡散されるということもあり得ます。

アジャイル、DevOps時代のテスト量

提供スピードも重要な品質な一つとした場合は、テスト量を単純に増やすというアプローチを取っていていると、気が付かない品質の特性を一つ犠牲にしているということになります。

ではどうするのが良いのかというと、テストに関して以下を徹底的に考えることが重要になります。

  • 実施するべきテストを考える
  • 実施しないテストを考える
  • 効率的なテストを考える

テストの量に関していうと、足りない状態でもなく、余分な状態でもない状態を考えます。

「丁度良い量」、「ほどほどな量」という言い方することもありますが、ちょっと誤解される場合もあるかもしれません。
これらは80点くらいで良いというわけではなく、テストに関して言うと、100やるべきテストがあるとした場合、80でいいというわけではなく、かといって120を行うというわけでもなく、100のやるべきテストがあった場合、100を狙ってテストを実施していく必要があります。
(誤解を招かないように数字に%などは付けないで表現しています)

そのため、テストを改善する場合には、今まで行われていない足りないテストがあればテスト量としては増えるかもしれませんし、今まで余分なテストを実施している場合にはテスト量はむしろ減るかもしれません。

更にそれらのテストを効率的に行えるようにするために適切なテストフェーズでテストが行えるようにしたり(テスト実施タイミング)、効果的なテスト(テスト実施方法)を考えます。
そういったことを考える際には、テスタビリティなどもとても重要な要素になってきます。
自動テストに関して言うとメンテナンスコストなども考慮して、効果的なテストを考える必要があります。

過去にテストの十分性や量などについて、プロダクトのフェーズによってテスト量は増減するべきなのかを考えたことがあります。
私の個人的な考えとですが、達成するべきテストというのはプロダクトのフェーズによって大きくは変わらないと思っています。

新しいプロダクトであっても、テストが不十分でユーザーに影響があった場合、それらは企業ブランドなどのイメージダウンになる可能性があり得ますし、細かい機能追加がメインのプロダクトであっても、余分なテストを多く行うプロセスを採用していると、競合企業に提供のスピードで負けたり、バグ修正の提供スピードが落ちるということに繋がる可能性があります。

最適なテストを考えるには

良いテストを考える際には、単純にテスト量を増やすのではなく、テスト観点が足りない状態でもなく、余分な状態でもない必要十分な量で網羅されているかどうかを考えます。
テスト観点という用語は少し定義が曖昧ですが、テストするべきフィーチャーを考え、そのフィーチャーに関して必要十分なテスト条件や、テストモデルを考えます。

imtnd.hatenablog.com

テスト量のように簡単に数に変換できるものでないので、テスト条件をモデル化したり、テストモデルを作成し、カバレッジなどを定めて、評価することで、テストの質を評価し、改善して行くことになります。

テスト条件をモデル化する際には、テスト条件をツリー構造で整理すると良いです。
VSTePなども参考になると思います。
https://www.jasst.jp/symposium/jasst13tokyo/pdf/A2-4.pdf

一つのテスト条件はなるべく少ないテストケースで確認できるようにテスト条件や、テストモデルをテストケースにしていきます。

実際、このような方法でテスト条件を整理したり、そのテスト条件をどうテストするか、どうテストすると効率的にテストできるかを考えると、テスト量は増えるのではなく、トータルのテスト量は削減されるということも結構あると思っています。
ユニットテスト内に限っても、テスト技法を適切に使ったり、レイヤードアーキテクチャやクリーンアーキテクチャのレイヤーの振る舞いに着目してテストを行うなどの方針を決めることで、テストケース数を削減することができる可能性があります。

最適なテストをテスト計画でどのように計画するか

こういった全体のテストなどの方針を示すドキュメントは、テスト計画になると思っています。

全体的な効率的なテストを考えるためには、ソフトウェアのアーキテクチャや、テスト環境等、色々な物を把握しておく必要があります。
自分はE2Eテストのみが自分の範囲であるとか、APIテスト以上が自分の範囲である、自分はユニットテストしか行わない、という状況では全体を俯瞰して最適なテストを考えるのは難しくなります。

テスト計画を考えるには、そういったことをすべて理解できる人が行うのは理想になります。
フロントエンドや、バックエンド、QAや、開発者など、自分の作業範囲のみでテストを考えるのではなく、全てを俯瞰してテストを考える必要があります。

テスト計画にはテストの自動化も入ってきますので、直近の有期限のプロジェクト限定のテストを対象とするというわけではなくなります。
テスト計画は、自動化方針のようなプロダクトとしてのテスト計画があり、直近のプロジェクトでは、そのプロジェクト特有なプロジェクトとしてのテスト計画を考える必要があると思います。
プロダクトのテスト計画をベースとして、プロジェクトではそれにプロダクトのテスト計画をアドオンしていくイメージになると思います。

こうして、テスト計画で効率的なテストはどういったものか、過不足ないテストを実施するにはどうするかを考えていきます。

プロジェクトの初期にテスト計画を考える場合、そのテスト計画でのテスト条件は抽象的なものなると思います。そのため、具体的なテスト条件は、テスト分析やテスト設計でも考える必要があります。

まとめ

自分なりに考えを少しまとめて文章にしてみました。

過不足ないテストを考えるには、プロジェクトなどを全体的に俯瞰して把握する必要があります。
その際に求められるのは最低限色々なことが分かるT型人材のような人だと思っています。また、大規模になりすぎると全てを把握することが難しくなると思いますので、開発スコープを少なくする、逆コンウェイの法則でマイクロサービスを設計して、開発規模を少なくするなどの工夫も必要になると思います。

なお、この記事に書いている最適なテストの考え方については、AIなどの機械学習を用いた帰納的な判断を行うソフトウェアには適用できないと思います。
AIのテストについては、QA4AIなどの資料を参考にしてください。