重要なテストを書く:最も複雑なコードに最初に取り組む

公開: 2022-03-11

コード品質のトピックに関する多くの議論、記事、ブログがあります。 人々は言う-テスト駆動技術を使用してください! テストは、リファクタリングを開始するための「必須」です。 それはすべてクールですが、2016年であり、10年、15年、さらには20年前に作成された大量の製品とコードベースがまだ生産されています。 それらの多くがテストカバレッジの低いレガシーコードを持っていることは周知の事実です。

私は常にテクノロジーの世界の最先端、あるいは最先端に立ちたいと思っていますが、新しいクールなプロジェクトやテクノロジーに取り組んでいますが、残念ながらそれが常に可能であるとは限らず、古いシステムに対処しなければならないことがよくあります。 ゼロから開発するとき、あなたは創造者として行動し、新しい問題をマスターしていると言いたいです。 しかし、レガシーコードに取り組んでいるとき、あなたは外科医のようです。システムが一般的にどのように機能するかは知っていますが、患者があなたの「手術」を生き残るかどうかはわかりません。 また、これはレガシーコードであるため、信頼できる最新のテストは多くありません。 これは、非常に頻繁に、最初のステップの1つがテストでカバーすることであることを意味します。 より正確には、カバレッジを提供するだけでなく、テストカバレッジ戦略を開発するためです。

結合と循環的複雑度:よりスマートなテストカバレッジのメトリクス

100%のカバレッジを忘れてください。 壊れやすいクラスを特定することで、よりスマートにテストします。
つぶやき

基本的に、私が決定する必要があるのは、システムのどの部分(クラス/パッケージ)を最初にテストでカバーする必要があるか、単体テストが必要な場所、統合テストがより役立つかなどです。確かに多くの方法があります。このタイプの分析にアプローチし、私が使用したものは最善ではないかもしれませんが、それは一種の自動アプローチです。 私のアプローチが実装されると、実際に分析自体を実行するのに最小限の時間がかかります。さらに重要なことは、レガシーコード分析にいくらかの楽しみをもたらすことです。

ここでの主なアイデアは、結合度(つまり、求心性結合度、つまりCA)と複雑度(つまり、循環的複雑度)の2つのメトリックを分析することです。

最初の1つは、クラスを使用するクラスの数を測定するため、基本的に、特定のクラスがシステムの中心にどれだけ近いかを示します。 クラスを使用するクラスが多いほど、テストでカバーすることが重要になります。

一方、クラスが非常に単純な場合(たとえば、定数のみが含まれている場合)、システムの他の多くの部分で使用されている場合でも、テストを作成することはそれほど重要ではありません。 ここで、2番目のメトリックが役立ちます。 クラスに多くのロジックが含まれている場合、循環的複雑度は高くなります。

同じロジックを逆に適用することもできます。 つまり、クラスが多くのクラスで使用されておらず、特定のユースケースを1つだけ表している場合でも、内部ロジックが複雑な場合は、テストでカバーすることは理にかなっています。

ただし、注意点が1つあります。2つのクラスがあるとします。1つはCA 100と複雑度2で、もう1つはCA60と複雑度20です。最初のメトリックの合計は高くなりますが、確実にカバーする必要があります。最初に2番目のもの。 これは、最初のクラスが他の多くのクラスで使用されているためですが、それほど複雑ではありません。 一方、2番目のクラスは他の多くのクラスでも使用されていますが、最初のクラスよりも比較的複雑です。

要約すると、CAと循環的複雑度が高いクラスを特定する必要があります。 数学的には、評価として使用できる適応度関数(f(CA、Complexity))が必要です。この関数の値は、CAおよび複雑度とともに増加します。

一般的に、2つのメトリック間の差異が最小のクラスには、テストカバレッジの最高の優先順位を与える必要があります。

コードベース全体のCAと複雑さを計算し、この情報をCSV形式で抽出する簡単な方法を提供するツールを見つけることは、困難であることがわかりました。 検索中に、無料の2つのツールに出くわしたので、それらは言うまでもなく不公平です。

  • 結合メトリクス:www.spinellis.gr/sw/ckjm/
  • 複雑さ:cyvis.sourceforge.net/

数学のビット

ここでの主な問題は、CAと循環的複雑度の2つの基準があるため、それらを組み合わせて1つのスカラー値に変換する必要があることです。 わずかに異なるタスクがある場合(たとえば、基準の組み合わせが最悪のクラスを見つける場合)、古典的な多目的最適化の問題が発生します。

いわゆるパレートフロント(上の写真の赤)でポイントを見つける必要があります。 パレートセットの興味深い点は、セット内のすべてのポイントが最適化タスクのソリューションであるということです。 赤い線に沿って移動するときはいつでも、基準の間で妥協する必要があります。一方が良くなると、もう一方は悪くなります。 これはスカラリゼーションと呼ばれ、最終的な結果はそれをどのように行うかによって異なります。

ここで使用できるテクニックはたくさんあります。 それぞれに長所と短所があります。 ただし、最も一般的なものは線形スカラー化と参照点に基づくものです。 線形は最も簡単なものです。 私たちの適応度関数は、CAと複雑さの線形結合のように見えます。

f(CA、複雑さ)=A×CA+B×複雑さ

ここで、AとBはいくつかの係数です。

最適化問題の解決策を表すポイントは、線上にあります(下の写真の青)。 より正確には、青い線と赤いパレートフロントの交差点になります。 私たちの元々の問題は、正確には最適化問題ではありません。 むしろ、ランキング関数を作成する必要があります。 ランキング関数の2つの値、基本的にはランク列の2つの値について考えてみましょう。

R1 = A ∗ CA + B ∗複雑さおよびR2 = A ∗ CA + B ∗複雑さ

上記の式は両方とも線の方程式であり、さらにこれらの線は平行です。 より多くのランク値を考慮に入れると、より多くの線が得られるため、パレート線が(点線の)青い線と交差する点がより多くなります。 これらのポイントは、特定のランク値に対応するクラスになります。

残念ながら、このアプローチには問題があります。 どのライン(ランク値)でも、CAが非常に小さく、複雑さが非常に大きい(またはその逆の)ポイントがあります。 これにより、メトリック値の間に大きな違いがあるポイントがリストの一番上に配置されます。これは、まさに避けたかったことです。

スカラー化を行うもう1つの方法は、参照ポイントに基づいています。 参照ポイントは、両方の基準の最大値を持つポイントです。

(max(CA)、max(Complexity))

適応度関数は、参照点とデータ点の間の距離になります。

f(CA、Complexity)=√((CA-CA) 2 +(Complexity-Complexity) 2

この適応度関数は、中心を基準点とする円と考えることができます。 この場合の半径は、ランクの値です。 最適化問題の解決策は、円がパレート前面に接する点です。 元の問題の解決策は、次の図に示すように、さまざまな円の半径に対応する点のセットになります(さまざまなランクの円の一部は青い点線の曲線で示されています)。

このアプローチは極値をより適切に処理しますが、まだ2つの問題があります。1つ目–線形結合で直面した問題をよりよく克服するために、参照ポイントの近くにポイントを増やしたい。 2番目– CAと循環的複雑度は本質的に異なり、異なる値が設定されているため、それらを正規化する必要があります(たとえば、両方のメトリックのすべての値が1から100になるように)。

これは、最初の問題を解決するために適用できる小さなトリックです。CAと循環的複雑度を調べる代わりに、それらの反転値を見ることができます。 この場合の基準点は(0,0)になります。 2番目の問題を解決するために、最小値を使用してメトリックを正規化することができます。 外観は次のとおりです。

反転および正規化された複雑さ– NormComplexity:

(1 + min(複雑さ))/(1 +複雑さ)∗ 100

反転および正規化されたCA– NormCA:

(1 + min(CA))/(1 + CA)∗ 100

注: 0による除算がないことを確認するために、1を追加しました。

次の図は、値が反転したプロットを示しています。

最終ランキング

私たちは今、最後のステップ、つまりランクの計算に来ています。 前述のように、私は参照点メソッドを使用しているので、ベクトルの長さを計算して正規化し、クラスの単体テストを作成することの重要性を考慮して昇格させるだけです。 最終的な式は次のとおりです。

ランク(NormComplexity、NormCA)= 100 −√(NormComplexity 2 + NormCA 2 )/√2

その他の統計

追加したいもう1つの考えがありますが、最初にいくつかの統計を見てみましょう。 カップリングメトリクスのヒストグラムは次のとおりです。

この図で興味深いのは、CAが低い(0〜2)クラスの数です。 CA 0のクラスは、まったく使用されていないか、トップレベルのサービスです。 これらはAPIエンドポイントを表しているので、たくさんあるのは問題ありません。 ただし、CA 1のクラスはエンドポイントによって直接使用されるクラスであり、エンドポイントよりも多くのクラスがあります。 これは、アーキテクチャ/設計の観点からどういう意味ですか?

一般に、これは一種のスクリプト指向のアプローチがあることを意味します。すべてのビジネスケースを個別にスクリプト化します(ビジネスケースが多すぎるため、コードを実際に再利用することはできません)。 その場合、それは間違いなくコードの臭いであり、リファクタリングを行う必要があります。 それ以外の場合は、システムの凝集度が低いことを意味します。この場合、リファクタリングも必要ですが、今回はアーキテクチャのリファクタリングが必要です。

上記のヒストグラムから得られる追加の有用な情報は、単体テストの対象となるクラスのリストから、結合度の低いクラス({0,1}のCA)を完全に除外できることです。 ただし、同じクラスが統合/機能テストの候補として適しています。

私が使用したすべてのスクリプトとリソースは、このGitHubリポジトリ(ashalitkin / code-base-stats)にあります。

それは常に機能しますか?

必ずしも。 まず第一に、それは実行時間ではなく、静的分析に関するものです。 クラスが他の多くのクラスからリンクされている場合、それは頻繁に使用されていることを示している可能性がありますが、常に正しいとは限りません。 たとえば、この機能がエンドユーザーによって本当に頻繁に使用されているかどうかはわかりません。 第2に、システムの設計と品質が十分に優れている場合は、システムのさまざまな部分/レイヤーがインターフェイスを介して分離されている可能性が高いため、CAの静的分析では実際の状況を把握できません。 これが、CAがSonarのようなツールでそれほど人気が​​ない主な理由の1つだと思います。 幸いなことに、これは特に古い醜いコードベースに適用することに関心があるので、私たちにとってはまったく問題ありません。

一般に、ランタイム分析の方がはるかに優れた結果が得られると思いますが、残念ながら、それははるかにコストがかかり、時間がかかり、複雑であるため、私たちのアプローチは潜在的に有用で低コストの代替手段です。

関連:単一責任の原則:優れたコードのレシピ