単体テストの期待される結果はハードコーディングされるべきですか?


29

単体テストの期待される結果をハードコーディングする必要がありますか、それとも初期化された変数に依存できますか?ハードコードまたは計算された結果は、単体テストでエラーを導入するリスクを高めますか?私が考慮していない他の要因はありますか?

たとえば、これら2つのうち、どちらがより信頼性の高い形式ですか?

[TestMethod]
public void GetPath_Hardcoded()
{
    MyClass target = new MyClass("fields", "that later", "determine", "a folder");
    string expected = "C:\\Output Folder\\fields\\that later\\determine\\a folder";
    string actual = target.GetPath();
    Assert.AreEqual(expected, actual,
        "GetPath should return a full directory path based on its fields.");
}

[TestMethod]
public void GetPath_Softcoded()
{
    MyClass target = new MyClass("fields", "that later", "determine", "a folder");
    string expected = "C:\\Output Folder\\" + string.Join("\\", target.Field1, target.Field2, target.Field3, target.Field4);
    string actual = target.GetPath();
    Assert.AreEqual(expected, actual,
        "GetPath should return a full directory path based on its fields.");
}

編集1: DXMの答えに応じて、オプション3は好ましいソリューションですか?

[TestMethod]
public void GetPath_Option3()
{
    string field1 = "fields";
    string field2 = "that later";
    string field3 = "determine";
    string field4 = "a folder";
    MyClass target = new MyClass(field1, field2, field3, field4);
    string expected = "C:\\Output Folder\\" + string.Join("\\", field1, field2, field3, field4);
    string actual = target.GetPath();
    Assert.AreEqual(expected, actual,
        "GetPath should return a full directory path based on its fields.");
}

2
両方してください。真剣に。テストは重複する可能性があります。また、ハードコードされた値を扱っている場合は、何らかのデータ駆動型テストを検討してください。
仕事

3番目のオプションが私が使用したいものであることに同意します。コンパイル時の操作を排除するため、オプション1が害になるとは思いません。
kwelch

\\:あなたのオプションのどちらも、かかわらず、ハードコーディングとテストはC上で実行されていない場合、壊れます使用
Qwertie

回答:


27

期待値を計算すると、より堅牢で柔軟なテストケースが得られると思います。また、期待される結果を計算する式で適切な変数名を使用することにより、最初に期待される結果がどこから来たのかがより明確になります。

そうは言っても、特定の例では、計算の入力としてSUT(テスト対象システム)を使用するため、「Softcoded」メソッドは信頼しません。MyClassにフィールドが適切に保存されていないバグがある場合、期待値の計算ではtarget.GetPath()のように間違った文字列が使用されるため、テストは実際に合格します。

私の提案は、理にかなっている期待値を計算することですが、計算がSUT自体のコードに依存しないことを確認してください。

OPの更新に対する応答:

はい、TDDの経験がある程度限られているため、オプション3を選択します。


1
いい視点ね!テストで未検証のオブジェクトに依存しないでください。
Hand-E-Food

それはSUTコードの複製ではありませんか?
アビックス

1
ある意味ではそうですが、それがSUTが機能していることを確認する方法です。同じコードを使用して破壊された場合、あなたは決して知りません。もちろん、計算を実行するために多くのSUTを複製する必要がある場合は、値#1をハードコーディングするだけで、オプション#1が改善される可能性があります。
DXM

16

コードが次の場合:

MyTarget() // constructor
{
   Field1 = Field2 = Field3 = Field4 = "";
}

2番目の例はバグをキャッチしませんが、最初の例はバグをキャッチします。

一般に、バグを隠す可能性があるため、ソフトコーディングはお勧めしません。例えば:

string expected = "C:\\Output Folder" + string.Join("\\", target.Field1, target.Field2, target.Field3, target.Field4);

問題を見つけることができますか?ハードコーディングされたバージョンで同じ間違いを犯すことはありません。ハードコードされた値よりも計算を正確にするのは難しいです。そのため、ソフトコーディングされた値よりもハードコーディングされた値を使用する方が好きです。

しかし、例外があります。コードをWindowsとLinuxで実行する必要がある場合はどうなりますか?パスは異なる必要があるだけでなく、異なるパス区切り文字を使用する必要があります!の違いを抽象化する関数を使用してパスを計算することは、そのコンテキストで意味があります。


私はあなたが言っていることを聞きます。ソフトコーディングは、他のテストケース(ConstructorShouldCorrectlyInitialiseFieldsなど)のパスに依存しています。あなたが説明する失敗は、失敗する他の単体テストによって相互参照されます。
Hand-E-Food

@ Hand-E-Food、オブジェクトの個々のメソッドのテストを書いているようです。しないでください。個々のメソッドではなく、オブジェクト全体の正確さをチェックするテストを作成する必要があります。そうしないと、オブジェクト内の変更に関してテストが脆弱になります。
ウィンストンイーバート

従うかどうかわかりません。私が与えた例は、純粋に仮説であり、理解しやすいシナリオでした。クラスとオブジェクトのパブリックメンバーをテストするユニットテストを書いています。それはそれらを使用する正しい方法ですか?
Hand-E-Food

@ Hand-E-Food、私が正しく理解していれば、テストのConstructShouldCorrectlyInitialiseFieldsはコンストラクターを呼び出し、フィールドが正しく設定されていることをアサートします。しかし、そうすべきではありません。内部フィールドが何をしているか気にする必要はありません。オブジェクトの外部の振る舞いが正しいと断言するだけです。そうしないと、内部実装を置き換える必要がある日が来るかもしれません。内部状態についてアサーションを行った場合、すべてのテストが失敗します。ただし、外部の動作についてのみアサーションを作成した場合は、すべてが機能します。
ウィンストン・エワート

@ Winston--私は実際にxUnit Test Patternsの本を読み進めており、その前にThe Art of Unit Testingを完成させています。私は自分が話していることを知っているふりをするつもりはありませんが、それらの本から何かを選んだと思いたいです。両方の本は、各テスト方法で絶対最小値をテストし、オブジェクト全体をテストするために多くのテストケースを用意することを強く推奨しています。インターフェースまたは機能が変更された場合、ほとんどのテストメソッドではなく、少数のテストメソッドのみを修正することを期待する必要があります。そして、それらは小さいので、変更は簡単になるはずです。
DXM

4

私の意見では、どちらの提案も理想的ではありません。理想的な方法は次のとおりです。

[TestMethod]
public void GetPath_Hardcoded()
{
    const string f1 = "fields"; const string f2 = "that later"; 
    const string f3 = "determine"; const string f4 = "a folder";

    MyClass target = new MyClass( f1, f2, f3, f4 );
    string expected = "C:\\Output Folder\\" + string.Join("\\", f1, f2, f3, f4);
    string actual = target.GetPath();
    Assert.AreEqual(expected, actual,
        "GetPath should return a full directory path based on its fields.");
}

つまり、テストは、オブジェクトの内部状態ではなく、オブジェクトの入力と出力のみに基づいて機能する必要があります。オブジェクトはブラックボックスとして扱う必要があります。(これは単なる例であるため、Path.Combineの代わりにstring.Joinを使用するなど、他の問題は無視します。)


1
すべてのメソッドが機能するわけではありません。多くの場合、いくつかのオブジェクトの状態を変更する副作用が正しくあります。副作用のあるメソッドの単体テストでは、おそらくメソッドの影響を受けるオブジェクトの状態を評価する必要があります。
マシューフリン

次に、その状態はメソッドの出力と見なされます。このサンプルテストの目的は、MyClassのコンストラクターではなく、GetPath()メソッドをチェックすることです。@DXMの答えを読んで、彼はブラックボックスアプローチを採用する非常に良い理由を提供します。
マイクナキス

@MatthewFlynn、その状態の影響を受けるメソッドをテストする必要があります。正確な内部状態は、実装の詳細であり、テストの業務ではありません。
ウィンストンイーバート

@MatthewFlynnは、単に明確にするために、示されている例に関連しているのでしょうか、それとも他の単体テストで考慮すべきものですか 私のような何かのためにその異物感を見ることができましたtarget.Dispose(); Assert.IsTrue(target.IsDisposed);(非常に簡単な例。)
ハンド-E-食品

この場合でも、IsDisposedプロパティは、クラスのパブリックインターフェイスの不可欠な部分であり、実装の詳細ではありません。(IDisposeインタフェースは、このような性質を提供していませんが、それは残念だ。)
マイクNakis

2

ディスカッションには2つの側面があります。

1.テストケースにターゲット自体を使用する
最初の質問は、クラス自体使用して、テストスタブで行われた作業に依存し、その一部を取得する必要があるかどうかです。- 一般に、テストしているコードについて仮定するべきではないため、答えは「いいえ」です。これが適切に行われないと、時間が経つにつれてバグがユニットテストの影響を受けなくなります。

2. ハードコーディングする
必要がありますか?再び答えはいいえです。なぜなら、他のソフトウェアのように-物事が進化するとき、彼の情報のハードコーディングは難しくなります。たとえば、上記のパスを再度変更する場合は、追加のユニットを記述するか、変更を続ける必要があります。より良い方法は、簡単に適応できる個別の構成から派生した入力および評価日付を保持することです。

たとえば、ここで私はテストスタブを正しくする方法です。

[TestMethod]
public void GetPath_Tested(int CaseId)
{
    testParams = GetTestConfig(caseID,"testConfig.txt"); // some wrapper that does read line and chops the field. 
    MyClass target = new MyClass(testParams.field1, testParams.field2);
    string expected = testParams.field5;
    string actual = target.GetPath();
    Assert.AreEqual(expected, actual,
        "GetPath should return a full directory path based on its fields.");
}

0

考えられる概念はたくさんあります。違いを見るためにいくつかの例を作りました

[TestMethod]
public void GetPath_Softcoded()
{
    //Hardcoded since you want to see what you expect is most simple and clear
    string expected = "C:\\Output Folder\\fields\\that later\\determine\\a folder";

    //If this test should also use a mocked filesystem it might be that you want to use
    //some base directory, which you could set in the setUp of your test class
    //that is usefull if you you need to run the same test on different environments
    string expected = this.outputPath + "fields\\that later\\determine\\a folder";


    //another readable way could be interesting if you have difficult variables needed to test
    string fields = "fields";
    string thatLater = "that later";
    string determine = "determine";
    string aFolder = "a folder";
    string expected = this.outputPath + fields + "\\" + thatLater + "\\" + determine + "\\" + aFolder;
    MyClass target = new MyClass(fields, thatLater, determine, aFolder);

    //in general testing with real words is not needed, so code could be shorter on that
    //for testing difficult folder names you write a separate test anyway
    string f1 = "f1";
    string f2 = "f2";
    string f3 = "f3";
    string f4 = "f4";
    string expected = this.outputPath + f1 + "\\" + f2 + "\\" + f3 + "\\" + f4;
    MyClass target = new MyClass(f1, f2, f3, f4);

    //so here we start to see a structure, it looks more like an array of fields
    //so what would make testing more interesting with lots of variables is the use of a data provider
    //the data provider will re-use your test with many different kinds of inputs. That will reduce the amount of duplication of code for testing
    //http://msdn.microsoft.com/en-us/library/ms182527.aspx


    The part where you compare already seems correct
    MyClass target = new MyClass(fields, thatLater, determine, aFolder);

    string actual = target.GetPath();
    Assert.AreEqual(expected, actual,
        "GetPath should return a full directory path based on its fields.");
}

要約すると:一般に、最初のちょうどハードコードされたテストは、単純で、ポイントにまっすぐであるなどの理由で、私にとって最も意味があります。

さらに構造化されたテストについては、データソースをチェックアウトして、テストの状況がさらに必要な場合にデータ行を追加できるようにします。


0

最新のテストフレームワークでは、メソッドにパラメーターを提供できます。私はそれらを活用します:

[TestCase("fields", "that later", "determine", "a folder", @"C:\Output Folder\fields\that later\determine\a folder")]
public void GetPathShouldReturnFullDirectoryPathBasedOnItsFields(
    string field1, string field2, string field3, string field,
    string expected)
{
    MyClass target = new MyClass(field1, field2, field3, field4);
    string actual = target.GetPath();
    Assert.AreEqual(expected, actual,
        "GetPath should return a full directory path based on its fields.");
}

私の見解では、これにはいくつかの利点があります。

  1. 開発者は多くの場合、コードの表面上単純な部分をSUTからユニットテストにコピーしたいと思われます。ウィンストンが指摘しているように、それらには依然としてトリッキーなバグが潜んでいる可能性があります。期待される結果を「ハードコーディング」すると、元のコードが間違っているのと同じ理由でテストコードが間違っている状況を回避できます。ただし、要件の変更により、多数のテストメソッドに埋め込まれたハードコーディングされた文字列を追跡する必要がある場合、それは迷惑な場合があります。テストロジック以外のすべてのハードコーディングされた値を1か所に保持すると、両方の長所が得られます。
  2. 1行のコードで、さまざまな入力と予想される出力のテストを追加できます。これにより、テストコードをDRYのまま維持しやすくしながら、より多くのテストを記述することができます。テストを追加するのはとても安いので、新しいテストケースに心が開かれているので、まったく新しいメソッドを記述しなければならないとは思いませんでした。たとえば、入力の1つにドットが含まれている場合、どのような動作が予想されますか?バックスラッシュ?空だったらどうしますか?または空白?または、空白で開始または終了しましたか?
  3. テストフレームワークは、各TestCaseを独自のテストとして扱い、提供された入力と出力をテスト名に含めます。すべてのTestCaseが1つをパスした場合、どのテストケースが壊れたのか、他のすべてのテストケースとどのように違うのかを簡単に確認できます。
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.