web-dev-qa-db-ja.com

メソッドがすべてプライベートである場合、大きなメソッドを小さなメソッドに分割すると、どのようにユニットテストが改善されますか?

私は現在、Joost VisserによるBuilding Maintainable Softwareと、推奨されるいくつかのメンテナンスガイドラインを読んでいます。A)各ユニット/メソッドは短く(メソッドごとに15行未満)、B)メソッドは低い 循環的複雑度 。これらの両方のガイドラインがテストに役立つことを示唆しています。

以下の例は、メソッドごとの循環的複雑度を減らすために複雑なメソッドをリファクタリングする方法を説明する本からの抜粋です。

前:

public static int calculateDepth(BinaryTreeNode<Integer> t, int n) {
    int depth = 0;
    if (t.getValue() == n) {
        return depth;
    } else {
        if (n < t.getValue()) {
            BinaryTreeNode<Integer> left = t.getLeft();
            if (left == null) {
                throw new TreeException("Value not found in tree!");
            } else {
                return 1 + calculateDepth(left, n);
            }
        } else {
             BinaryTreeNode<Integer> right = t.getRight();
             if (right == null) {
                throw new TreeException("Value not found in tree!");
             } else {
                return 1 + calculateDepth(right, n);
             }
        }
    }
}

後:

public static int calculateDepth(BinaryTreeNode<Integer> t, int n) {
     int depth = 0;
     if (t.getValue() == n)
        return depth;
     else
        return traverseByValue(t, n);
}
private static int traverseByValue(BinaryTreeNode<Integer> t, int n) {
     BinaryTreeNode<Integer> childNode = getChildNode(t, n);
     if (childNode == null) {
        throw new TreeException("Value not found in tree!");
     } else {
        return 1 + calculateDepth(childNode, n);
     }
}
private static BinaryTreeNode<Integer> getChildNode(
     BinaryTreeNode<Integer> t, int n) {
     if (n < t.getValue()) {
        return t.getLeft();
     } else {
        return t.getRight();
     }
}

彼らの正当化で彼らは述べる(私の強調):

引数:

「McCabe 15の1つのメソッドをMcCabe 5の3つのメソッドにそれぞれ置き換えると、McCabe全体は依然として15のままです(したがって、全体で15のコントロールブランチがあります)。したがって、何も得られません。」

カウンター引数:

もちろん、メソッドをいくつかの新しいメソッドにリファクタリングしても、システムの全体的なMcCabeの複雑さを軽減することはできません。しかし、保守性の観点からは、そうすることには利点があります:テストしやすくなり、作成されたコードを理解できます。したがって、すでに述べたように、新しく作成された単体テストを使用すると、失敗したテストの根本原因をより簡単に特定できます。

質問:テストはどのように簡単になりますか?

this 質問、 this 質問、 this 質問、 this 質問、 this ブログへの回答です。それらを使用するパブリックメソッドを介してそれらをテストする必要があります。したがって、本の例に戻ると、パブリックメソッドを介してプライベートメソッドをテストしている場合、ユニットテスト機能はどのように改善されるか、またはそのためにまったく変更されますか。案件?

9
Adrian773

たくさんのテストを書いた後、私は大きなメソッドを分割し、プライベートメソッドをテストすることに強く賛成します。機能を小さなステップに分割することには、2つの大きな利点があります。

  1. 操作の名前を導入することにより、コードはより自己文書化されます。

  2. より小さな方法を使用することにより、コードはより単純になり、したがってより正確になる可能性が高くなります。例えば。 getChildNode()をすぐに理解できます。

プログラム全体の循環的複雑度は軽減されませんが、これら2つの利点は、私の本にある追加のコードよりも優れています。 3つ目の利点があります。privateアクセス修飾子を回避できると仮定すると、コードのテストがはるかに簡単になります。

プライベート実装の詳細をテストしないようにアドバイスする人は良い点を示します。テストは、実装がパブリックインターフェイスに準拠していることを示す必要がありますが、これはパブリックメソッドのブラックボックステストによってのみ実行できます。このようなテストでは、100%のカバレッジを取得する必要はありませんが、カバレッジが不足している場合は、仕様で不要なデッドコードを示しています。 TDDテストは外部から観察可能な動作を定義する必要があるため、このようなテストはこのカテゴリに分類されます。

ただし、テストに別のアプローチを使用することもできます。既存の実装がおそらく正しいことを示し、プログラマーが期待したとおりに機能することを示します。すでにコードがあるので、カバレッジを最大化するようにテストを設計できます。境界値を使用して、プログラムの動作を細かく調整できます。つまり、テスト対象のシステムの実装の詳細を知っているホワイトボックステストを作成できます。これらのテストは実装と高度に結び付いていますが、より一般的なブラックボックステストもある限り問題ありません。

結果として、私は「ハッピーパス」とインターフェースの基本的な保証をたどるいくつかのブラックボックステストを好みます。しかし、すべての可能性を実行するのは面倒な方法です。関数内の各入力パラメーターまたは状態変数を使用すると、テストスペースが指数関数的に増加します。関数f(bool)は、関数f(bool, bool)2²= 4とf(bool, bool, bool, bool)の2つのテストを受ける可能性があります24= 16。これは耐えられない。しかし、大きな関数を小さな関数に分割することで、小さな各関数が期待どおりに機能し、関数が正しく機能することを示すだけで済みます(このテストを帰納法と呼びます)。これで、私のワークロードが増加するのではなく、追加されます。徹底したい場合は、大幅に改善されます。


具体的な例では、最初の試行では重複したテストを必要とするコードの重複があり、2番目の試行ではモックできない関数間に相互依存関係があるため、どちらの可能性も最適ではありません。テストが簡単なのはgetChildNode()だけですが、nt.getValue()と等しい場合、この関数は正しくありません。これは、その関数がtraverseByValue()。簡単にテストできる代替案は次のとおりです。

_public static int calculateDepth(BinaryTreeNode<Integer> t, int n) {
    if (t == null) {
        throw new TreeException("Value not found in tree!");
    }

    if (t.getValue() == n) {
        return 0;
    }

    BinaryTreeNode<Integer> child = null;
    if (n < t.getValue()) {
        child = t.getLeft();
    } else {
        child = t.getRight();
    }

    return 1 + calculateDepth(child, n);
}
_

この特定のケースでは、ヘルパー関数を使用していません。これは、使用せずに実行できるほど単純だからです。このコードには4つのパスしかありません。以前の実装では、他のブランチがthrowまたはreturnによってすでに終了されている場合に条件をネストし、不要なコードの重複によってこれを隠していました。

ただし、再帰またはループのテストは困難な場合があります。かなり複雑なツリーを作成して正しい結果を確認することはできますが、ループ不変条件を確認する方法が必要です。高階関数を持つ言語では、次のようになります。

_public static int calculateDepth(
        BinaryTreeNode<Integer> t,
        int n)
{
    return calculateDepthLoop(t, n, calculateDepthLoop);
}

type Recurser = int(BinaryTreeNode<Integer>, int, Recurser);

private static int calculateDepthLoop(
         BinaryTreeNode<Integer> t,
         int n,
         Recurser recurse)
{
    if (t == null) {
        throw new TreeException("Value not found in tree!");
    }

    if (t.getValue() == n) {
        return 0;
    }

    BinaryTreeNode<Integer> child = null;
    if (n < t.getValue()) {
        child = t.getLeft();
    } else {
        child = t.getRight();
    }

    return 1 + recurse(child, n, recurse);
}
_

これで、次のようなテスト計画を実行できます。

  • calculateDepthLoop(null, ANY, ANY)がスローされます。
  • calculateDepthLoop(Tree(x, ANY, ANY), x, ANY) is 0`。
  • _y < x_のcalculateDepthLoop(Tree(x, left, ANY), y, callback)result = callback(left, y, callback)を呼び出し、_result + 1_を返します。
  • _x < y_のcalculateDepthLoop(Tree(x, ANY, right), y, callback)result = callback(right, y, callback)を呼び出し、_result + 1_を返します。

4つのテスト(パスごとに1つ)だけで、calculateDepthLoop()が期待どおりに機能することを確認できます。 xyのすべての有効な値ですべてが機能することを確認するために、さらに2、3必要になる場合があります。これで、すべてが正しく統合されていることを確認する別のテストだけが必要です。これは、calculateDepth()のブラックボックステストで実行できます。これは、再帰する関数を必要とする適度に複雑なツリーを作成することで行います左と右の両方といくつかの値を返します。

8
amon

これは非常に良い質問で、しばらくの間私を悩ませてきました。

次の可能性があります。

  1. 示されているようにプライベートメソッドに分割しますが、次にアクセス修飾子をプライベートからパッケージ保護に変更し、独自の単体テストでそのような各メソッドをテストします

  2. 静的な「ヘルパー」クラスのメソッドを抽出し、そのクラスをPowermockitoでモックしますが、その静的クラスのメソッドをそれぞれの単体テストでカバーします

  3. 別のクラスの静的でないメソッドのコレクションからメソッドを抽出し、Mockitoでそれをモックします。

ケース1ではpublicメソッドもテストする必要があり、テストデータを2回準備することになるため、これらはどれもうまく機能していないようです(保護されたメソッドに対して1回、パブリックに対して1回)。しかし、これはリファクタリングで克服できます。ユニットテストの共通機能をユニットテストヘルパーメソッドに組み込みます。

0
ACV

しかし、保守性の観点からは、そうすることには利点があります。作成されたコードのテストと理解が容易になります。

最初に2番目のクレームから始めましょう。

コードがわかりやすくなります

これまでの議論では、この点は多かれ少なかれ受け入れられていると思います。

したがって、質問は次のようになります:コードが理解しやすくなると、テストが容易になりますか?

他の理由がなければ、特に失敗のリスクがあるこれらの入力と条件を特定することが容易になれば、明らかにそうだと思います。

0
Roger

小さなビットに分割するのではなく、関数自体を小さくして読みやすくすることをお勧めします。さらに、2番目の引数タイプを修正し、元のコードがクラッシュする引数tがnullの場合は例外をスローしました。さらに、不要な関数呼び出しもありません。さらに、コードの重複もありません。さらに、テストのみを行う1つの関数。

public static int calculateDepth(BinaryTreeNode<Integer> t, Integer n) {
    for (int depth = 0; t != null; ++depth) {
        Integer value = t.getValue();
        if (n == value)
            return depth;
        t = n < value ? t.getLeft() : t.getRight();
    }
    throw new TreeException("Value not found in tree!");
}
0
gnasher729

技術的には、テストは簡単にはなりません。大きくて複雑なメソッドをテストするために書かれたコードは、小さくてきれいでシンプルなメソッドをテストするために機能するはずです。ただし、理解しやすいので、テストがない場合は、テストが簡単になります。

プライベートメソッドは実際にはそれを助けません:テストを記述する必要はありません。プライベートメソッドは、元のメソッドの一部を抽象化したものです。他の場所でそれらを必要としたり使用したりすることはなく、メインメソッドをテストするためのコードを記述すると、(必然的に)プライベートメソッドがテストされます。これはすべて、コードを読みやすくするための問題です。

私は2つの可能性を見ることができます:

  • 作成者は、ユニットテストが予期せず失敗したときにスタックトレースに含まれるプライベートメソッド名を参照しています。

    したがって

    すでに述べたように、新しく作成された単体テストを使用すると、より簡単に特定失敗したテストの根本原因を特定できます。

    まだ私を困惑させているのは新しく書かれた単体テストです。彼はどういう意味ですか、そして彼がすでに述べたそれは何ですか?

  • 著者は、プライベートメソッドへのすばらしいリファクタリングに夢中になり、単独でテストすることが非常に難しいことを忘れていました。

後者だと思います。彼が言及する他の利点(より良い理解可能性)は依然として有効です。

0
guillaume31