web-dev-qa-db-ja.com

TDDでは、リファクタリングされたコードに単体テストを追加する必要がありますか?

テスト駆動開発(TDD)を使用してコードをリファクタリングしている間、作成している新しいリファクタリングされたコードの新しいテストケースを作成し続ける必要がありますか?

この質問は、次のTDDステップに基づいています。

  1. コードが失敗するための十分なテストを書く
  2. テストに合格するだけの十分なコードを記述します
  3. リファクタリング

私の疑問はリファクタリングの段階にあります。新しいユニットテストケースはリファクタリングされたコードのために書かれるべきですか?

それを説明するために、簡単な例を示します。


私はRPGを作成していて、次のことを行うHPContainerシステムを作成しているとします。

  • プレイヤーがHPを失うことを許可します。
  • HPがゼロを下回ってはいけません。

これに答えるために、以下のテストを作成します。

[Test]
public void LoseHP_LosesHP_DecreasesCurrentHPByThatAmount()
{
    int initialHP = 100;
    HPContainer hpContainer= new HPContainer(initialHP);
    hpContainer.Lose(5)
    int currentHP = hpContainer.Current();
    Assert.AreEqual(95, currentHP);
}
[Test]
public void LoseHP_LosesMoreThanCurrentHP_CurrentHPIsZero()
{
    int initialHP = 100;
    HPContainer hpContainer= new HPContainer(initialHP);
    hpContainer.Lose(200)
    int currentHP = hpContainer.Current();
    Assert.AreEqual(0, currentHP);
}

要件を満たすために、次のコードを実装します。

public class HPContainer
{
    private int currentHP = 0;

    public void HPContainer(int initialHP)
    {
        this.currentHP = initialHP; 
    }

    public int Current()
    {
        return this.currentHP;
    }

    public void Lose(int value)
    {
        this.currentHP -= value;
        if (this.currentHP < 0)
            this.currentHP = 0;
    }
}

良い!

テストに合格しています。

やった!


ここで、コードが大きくなり、そのコードをリファクタリングしたいとします。次のようにClamperクラスを追加することをお勧めします。

public static class Clamper
{
    public static int ClampToNonNegative(int value)
    {
        if(value < 0)
            return 0;
        return value;
    }
}

その結果、HPContainerクラスを変更します。

public class HPContainer
{
    private int currentHP = 0;

    public void HPContainer(int initialHP)
    {
        this.currentHP = initialHP; 
    }

    public int Current()
    {
        return this.currentHP;
    }

    public void Lose(int value)
    {
        this.currentHP = Clamper.ClampToNonNegative(this.currentHP - value);
    }
}

テストはまだ成功しているので、コードに回帰を導入しなかったと確信しています。

しかし、私の質問は:

ユニットテストをクラスに追加する必要がありますClamper


私は2つの反対する議論を見ます:

  1. はい、Clamperを回帰からカバーする必要があるため、テストを追加する必要があります。 Clamperを変更する必要がある場合、テストカバレッジで安全に変更できることが保証されます。

  2. いいえ、Clamperはビジネスロジックの一部ではなく、すでにHPContainerのテストケースでカバーされています。テストを追加しても、不要な混乱が生じ、将来のリファクタリングが遅くなるだけです。

TDDの原則と適切な慣行に従って、正しい推論は何ですか?

35
Albuquerque

前後のテスト

TDDでは、リファクタリングされたコードに単体テストを追加する必要がありますか?

「リファクタリングされたコード」は、リファクタリングしたテストを after に追加することを意味します。これには、変更をテストするポイントがありません。 TDDはテストに非常に依存していますbefore and afterコードの実装/リファクタリング/修正。

  • ユニットテストの結果がリファクタリングの前後で同じであることを証明できれば、リファクタリングによって動作が変更されなかったことを証明できます。
  • テストが失敗(前)から合格(後)に進んだ場合、実装/修正によって問題が解決されたことを証明しました。

単体テストを追加するべきではありませんリファクタリングではなく、(これらのテストがもちろん保証されていると仮定)。


リファクタリングは変更されていない動作を意味します

新しいユニットテストケースはリファクタリングされたコードのために書かれるべきですか?

まさに リファクタリングの定義 は、動作を変更せずにコードを変更することです。

リファクタリングは、既存のコード本体を再構築し、内部構造を変更するための統制のとれた手法です外部の動作を変更せずに

単体テストは動作をテストするために特別に記述されているため、リファクタリング後に追加の単体テストを要求することは意味がありません。

  • これらの新しいテストが関連している場合、それらはリファクタリングの前にすでに関連していました。
  • これらの新しいテストが適切でない場合は、明らかに必要ありません。
  • これらの新しいテストが関連していなかったが現在は関連している場合、リファクタリングによって常に動作が変更されている必要があります。つまり、リファクタリングだけではありません。

リファクタリングによって、以前は必要でなかった追加の単体テストが必要になることはありません。


テストを追加する必要がある場合があります

そうは言っても、最初から必要だったはずのテストが今まで忘れていた場合は、もちろんそれらを追加できます。以前にテストを書くのを忘れていたからといって、テストを追加できないということを私の答えにしないでください。

同様に、場合によってはケースをカバーするのを忘れて、バグに遭遇して初めて明らかになることがあります。次に、この問題のケースをチェックする新しいテストを作成することをお勧めします。


他のものを単体テストする

クラステストにユニットテストを追加する必要がありますか?

Clamperは、internalの非表示の依存関係であるため、HPContainerクラスである必要があります。 HPContainerクラスのコンシューマーは、Clamperが存在することを認識していないため、その必要はありません。

単体テストは、消費者に対する外部(パブリック)の行動にのみ焦点を当てています。 Clamperinternalである必要があるため、単体テストは必要ありません。

Clamperが完全に別のアセンブリにある場合は、パブリックであるため、ユニットテストが必要です。しかし、あなたの質問はこれが関連しているかどうかを不明確にします。

Sidenote
私はここでIoC説教全体を行うつもりはありません。一部の非表示の依存関係は、それらが純粋(つまり、ステートレス)であり、モックする必要がない場合に許容されます(例: .NETのMathクラスを挿入することを実際に強制している人はおらず、ClamperMathと機能的に同じです。
私は他の人が反対し、「すべてを注入する」アプローチを取ると確信しています。それが可能であることに私は反対していませんが、私の回答では、投稿された質問には関係がないため、この回答の焦点では​​ありません。


クランプ?

クランプ方法は、そもそも必要なものではないと思います。

_public static int ClampToNonNegative(int value)
{
    if(value < 0)
        return 0;
    return value;
}
_

ここで記述したのは、既存のMath.Max()メソッドのより限定されたバージョンです。すべての使用法:

_this.currentHP = Clamper.ClampToNonNegative(this.currentHP - value);
_

_Math.Max_で置き換えることができます:

_this.currentHP = Math.Max(this.currentHP - value, 0);
_

メソッドが単一の既存のメソッドのラッパーにすぎない場合、それを使用しても意味がありません。

50
Flater

これは次の2つのステップと見なすことができます。

  • first新しいパブリッククラスClamperを作成します(HPContainerを変更せずに)。これは実際にはリファクタリングではなく、厳密にTDDを適用する場合、文字通り TDDのナノサイクル に従うと、少なくとも記述する前に、このクラスのコードの最初の行を書き込むことさえ許可されません。 1つの単体テスト。

  • 次にHPContainerクラスを使用してClamperのリファクタリングを開始します。このクラスの既存の単体テストですでに十分なカバレッジが提供されていると仮定すると、このステップでこれ以上ユニットテストを追加する必要はありません。

したがって、yes、近い将来にリファクタリングに使用することを意図して再利用可能なコンポーネントを作成する場合は、コンポーネントの単体テストを追加する必要があります。そして、no、リファクタリングの間、通常はユニットテストを追加しません。

もう1つのケースは、Clamperがまだ再利用を目的としていないプライベート/内部に保持されている場合です。その場合、抽出全体を1つのリファクタリング手順と見なすことができ、新しい単体テストを追加しても必ずしもメリットはありません。ただし、これらのケースでは、コンポーネントの複雑さも考慮に入れます。2つのコンポーネントが非常に複雑で、テストが失敗する根本的な原因が原因で、両方のテストを特定するのが難しい場合は、次のことをお勧めします。両方に個別の単体テストを提供します。それ自体でClamperをテストする1セットのテストと、HPContainerのモックを挿入してClamperをテストする1セットのテスト。

21
Doc Brown

Clamperは独自のユニットであり、ユニットは他の場所で使用できるため、ユニットテストでテストする必要があります。 ClamperManaContainerFoodContainerDamageCalculatorなどを実装するのにも役立つ場合、これは素晴らしいことです。

Clamperが実装の詳細のみである場合、直接テストすることはできません。これは、テストのためにユニットとしてアクセスすることができないためです。

最初の例では、チェックを実装の詳細として扱います。これが、ifステートメントが単独で機能することを確認するテストを記述しなかった理由です。実装の詳細として、それをテストする唯一の方法は、それが実装の詳細であるユニットの観察可能な動作をテストすることです(この場合は、HPContainerの動作はLose(...)を中心にしています)。 。

リファクタリングを維持しながら、実装の詳細はそのままにします。

public class HPContainer
{
    private int currentHP = 0;

    public void HPContainer(int initialHP)
    {
        this.currentHP = initialHP; 
    }

    public int Current()
    {
        return this.currentHP;
    }

    public void Lose(int value)
    {
        this.currentHP = ClampToNonNegative(this.currentHP - value);
    }

    private static int ClampToNonNegative(int value)
    {
        if(value < 0)
            return 0;
        return value;
    }
}

表現力を与えますが、新しいユニットを導入する決定は後で行います。うまくいけば、再利用可能なソリューションを合理的に一般化できる複製のインスタンスがいくつかあるとき。現在(2番目の例)は、それが必要になると推定しています。

4
Kain0_0

いいえ、Clamperクラスのテストを記述しないでください。
HPContainerクラスのテストを通じてすでにテストされているため。

テストに合格するための最も簡単で迅速なソリューションを作成すると、すべてを実行する1つの大きなクラス/関数が作成されます。

リファクタリングを開始すると、実装の全体像を確認できるので、ロジックの重複や一部のパターンを認識することができます。
リファクタリング中に、専用のメソッドまたはクラスに重複を抽出して重複を削除します。

コンストラクターを介して新しく導入されたクラスを渡すことを決定した場合、新しい依存関係を渡すためにテストでクラスをセットアップするテストの1箇所のみを変更する必要があります。これは、リファクタリング中に「許可」されたテストコードの変更のみである必要があります。

リファクタリング中に導入されたクラスのテストを作成すると、「無限」ループになります。
新しいクラスのテストを強制的に作成したため、さまざまな実装で「プレイ」できません。これは、このクラスがメインクラスのテストを通じてすでにテストされているため、ばかげています。

ほとんどの場合、リファクタリングとは、重複または複雑なロジックをより読みやすく構造化された方法で抽出することです。

2
Fabio

クラステストにユニットテストを追加する必要がありますか?

未だに。

目標は、機能するクリーンなコードです。この目標に貢献しない儀式は無駄です。

テストではなく機能するコードに対して支払いを受けるので、私の哲学は、特定のレベルの信頼性に到達するためにテストをできるだけ少なくすることです Kent Beck、2008

リファクタリングは実装の詳細です。テスト中のシステムの外部動作はまったく変更されていません。この実装の詳細についてテストの新しいコレクションを作成しても、信頼性はまったく向上しません。

実装を新しい関数、新しいクラス、または新しいファイルに移す-コードの動作とは関係のない多くの理由で、これらのことを行います。まだ新しいテストスイートを導入する必要はありません。これらは動作ではなく構造の変化です

プログラマーテストは、動作の変化に敏感で、構造の変化に鈍感でなければなりません。 - ケントベック、2019

変更について考え始めるのは、Clamperの動作を変更することに関心があり、HPContainerを作成する追加の式が邪魔になり始めたときです。

バナナが欲しかったのですが、得たのはバナナとジャングル全体を抱えたゴリラでした。 -ジョーアームストロング

私たちのテスト(ソリューション内のいくつかのモジュールの予想される動作のドキュメントとして機能する)が、無関係な詳細の束で汚染される状況を回避しようとしています。現在のユースケースでは実際の実装は必要ありませんが、それらなしではコードを呼び出すことができないため、nullオブジェクトの束でテストサブジェクトを作成するテストの例を見たことがあるでしょう。

ただし、純粋に構造的なリファクタリングの場合、新しいテストの導入を開始する必要はありません。

2
VoiceOfUnreason

個人的には、リファクタリングの影響を受けない安定したインターフェース(外部または内部)に対してのみテストすることを信じています。リファクタリングを阻害するテストを作成するのは好きではありません(あまりにも多くのテストを壊してしまうため、リファクタリングを実装できないケースを見てきました)。コンポーネントまたはサブシステムが、特定のインターフェースを提供する他のコンポーネントまたはサブシステムと契約している場合は、そのインターフェースをテストします。インターフェースが純粋に内部的なものである場合は、それをテストしないでください。または、作業が終わったら、テストを破棄します。

1
Michael Kay

単体テストは、リファクタリング作業でバグが発生しなかったことをある程度保証するものです。

そのため、ユニットテストを作成し、既存のコードを変更せずにテストがパスすることを確認します。

次に、リファクタリングを行い、その間にユニットテストが失敗しないようにします。

これにより、リファクタリングによって問題が解決されなかったことがある程度確実になります。もちろん、ユニットテストが正しく、元のコードのすべての可能なコードパスをカバーしている場合にのみ当てはまります。テストで何かを見逃した場合でも、リファクタリングが物事を壊すリスクがあります。

0
jwenting

これは、私がテストとコードを構造化し、考えることを一般的に好む方法です。コードはフォルダーに編成する必要があります。フォルダーにはサブフォルダーがあり、さらに細かく分割できます。葉であるフォルダー(サブフォルダーはありません)はファイルと呼ばれます。また、テストは、メインコードの階層を反映する対応する階層に編成する必要があります。

フォルダーが意味をなさない言語では、パッケージ/モジュール/ etcまたは他の同様の階層構造で置き換えることができます。プロジェクトの階層要素が何であるかは関係ありません。ここで重要なのは、一致する階層でテストとメインコードを編成することです。

階層内のフォルダーのテストは、メインコードベースの対応するフォルダーの下のすべてのコードを完全にカバーする必要があります。階層の別の部分のコードを間接的にテストするテストは偶発的であり、他のフォルダーのカバレッジにはカウントされません。理想的には、階層のさまざまな部分からのテストによってのみ呼び出されてテストされるコードがあってはなりません。

テスト階層をクラス/関数レベルに細分することはお勧めしません。これは通常、粒度が細かすぎて、細部を細かく分割してもあまりメリットがありません。メインコードファイルが十分に大きく、複数のテストファイルが必要な場合は、通常、ファイルの処理が多すぎるため、分解する必要があることを示しています。

この組織構造の下で、新しいクラス/関数がそれを使用しているすべてのコードと同じリーフフォルダーの下にある場合、そのファイルのテストですでにカバーされている限り、独自のテストは必要ありません。一方、新しいクラス/メソッドが、階層内の独自のファイル/フォルダーを保証するのに十分な大きさまたは独立していると考える場合は、対応するテストファイル/フォルダーも作成する必要があります。

一般的に言えば、ファイルは大まかな輪郭を頭に収めることができる程度のサイズである必要があり、どこにファイルのコンテンツを説明するための段落を記述して、それらをまとめているかを説明できます。経験則として、これは通常、私には画面全体です(フォルダーには、画面全体のサブフォルダーを含めることはできません。ファイルには、画面レベルを超えるトップレベルのクラス/関数を含めることはできません。関数は、画面一杯以上の行があります)。ファイルのアウトラインを想像するのが難しいと感じる場合、ファイルはおそらく大きすぎます。

0
Lie Ryan

他の回答が指摘しているように、あなたが説明していることはリファクタリングのようには聞こえません。 TDDをリファクタリングに適用すると、次のようになります。

  1. APIサーフェスを特定します。定義により、リファクタリングはAPIサーフェスを変更しません。コードが明確に設計されたAPIサーフェスを使用せずに記述され、コンシューマーが実装の詳細に依存している場合、リファクタリングでは対処できない大きな問題が発生します。ここで、APIサーフェスを定義し、他のすべてをロックダウンし、メジャーバージョン番号を上げて、新しいバージョンに下位互換性がないことを示すか、プロジェクト全体を破棄して、最初から書き直します。

  2. APIサーフェイスに対するテストを記述します。保証の観点からAPIを考えてください。たとえば、メソッドFooは、指定された条件を満たすパラメータを指定すると意味のある結果を返し、それ以外の場合は特定の例外をスローします。特定できるすべての保証についてテストを記述します。 APIが実際に行うことではなく、APIが行うべきことについて考えてください。オリジナルの仕様やドキュメントがある場合は、それを調べます。なかった場合は、書きます。ドキュメンテーションのないコードは正しいか間違っていません。 API仕様にないものに対するテストを記述しないでください。

  3. コードの変更を開始し、テストを頻繁に実行して、APIの保証に違反していないことを確認します。

多くの組織では、開発者とテスターの間に断絶があります。少なくとも非公式にTDDを実践していない開発者は、コードをテスト可能にする特性に気付かないことがよくあります。すべての開発者がテスト可能なコードを記述した場合、フレームワークをモックする必要はありません。テストしやすいように設計されていないコードは、鶏と卵の問題を引き起こします。コードを修正するまで、テストなしでリファクタリングすることはできません。テストを作成することもできません。最初からTDDを実践しないことのコストは莫大です。変更には、元のプロジェクトよりもコストがかかる可能性があります。繰り返しになりますが、ここでは、重大な変更を行うか、すべてを破棄することに辞任します。

0
StackOverthrow