単体テストはどのように機能しますか?


23

私はコードをより堅牢にしようとしており、ユニットテストについて読んでいますが、実際の有用な使用法を見つけることは非常に困難です。たとえば、ウィキペディアの例

public class TestAdder {
    public void testSum() {
        Adder adder = new AdderImpl();
        assert(adder.add(1, 1) == 2);
        assert(adder.add(1, 2) == 3);
        assert(adder.add(2, 2) == 4);
        assert(adder.add(0, 0) == 0);
        assert(adder.add(-1, -2) == -3);
        assert(adder.add(-1, 1) == 0);
        assert(adder.add(1234, 988) == 2222);
    }
}

必要な結果を手動で計算してテストする必要があるため、このテストはまったく役に立たないと感じています。ここでのより良い単体テストは

assert(adder.add(a, b) == (a+b));

ただし、これはテストで関数自体をコーディングするだけです。誰かがユニットテストが実際に役立つ例を教えてもらえますか?参考までに、私は主に〜10のブール値といくつかのintを受け取り、これに基づいてintの結果を与える「プロシージャ」関数を主にコーディングしています。テスト。編集:また、これは(おそらく設計が不適切な)ルビーコード(私が作成しなかった)を移植しているときに正確にすべきでした


14
How does unit testing work?誰も本当に知りません:)
ヤンニス

30
「必要な結果を手動で計算する必要があります」。それはどうして「まったく役に立たない」のでしょうか?他にどのように答えが正しいと確信できますか?
S.Lott

9
S.Lott @:それは、現代の時代に、人々は必ずコンピュータが数字をクランチができ作るために時間を費やし、古代の人々は数字をクランチしたコンピュータを使用し、時間を節約、進捗と呼ばれています:D
コーダ

2
@Coder:ユニットテストの目的は「数字をクランチして時間を節約する」ことではありません;)
Andres F.

7
@lezebulon:ウィキペディアの例はあまり良くありませんが、それは特定のテストケースの問題であり、一般的な単体テストの問題ではありません。例のテストデータの約半分は新しいものを追加しないため、冗長になります(そのテストの作成者がより複雑なシナリオで何を行うかを考えるのは怖いです)。より意味のあるテストは、少なくとも次のシナリオでテストデータを分割します。「負の数を追加できますか?」、「ゼロニュートラルですか?」、「負の数と正の数を追加できますか?」。
アンドレスF.

回答:


26

ユニットテストは、十分に小さいユニットをテストしている場合、常に明白なことを断言します。

add(x, y)ユニットテストについても言及される理由は、いつか誰かが、addどこでもaddが使用されていることに気づかないコードを処理する特別な税務ロジックにアクセスするためです。

単体テストは連想原理に非常に近い:AがBを行い、BがCを行う場合、AはCを行う。「AはCを行う」は、より高いレベルのテストである。たとえば、次の完全に合法的なビジネスコードを検討してください。

public void LoginUser (string username, string password) {
    var user = db.FetchUser (username);

    if (user.Password != password)
        throw new Exception ("invalid password");

    var roles = db.FetchRoles (user);

    if (! roles.Contains ("member"))
        throw new Exception ("not a member");

    Session["user"] = user;
}

一見したところ、非常に明確な目的があるので、これは単体テストの素晴らしい方法のように見えます。ただし、約5つの異なる処理を実行します。それぞれに有効なケースと無効なケースがあり、単体テストの巨大な組み合わせを作成します。理想的には、これはさらに細分化されます:

public void LoginUser (string username, string password) {

    var user = _userRepo.FetchValidUser (username, password);

    _rolesRepo.CheckUserForRole (user, "member");

    _localStorage.StoreValue ("user", user);
}

これでユニットになりました。1つの単体テストで_userRepoFetchValidUser、の有効な動作を何が考慮されるかは気にしません。別のテストを使用して、有効なユーザーの構成を正確に確認できます。同様にCheckUserForRole...ロールの構造がどのように見えるかを知ることからテストを切り離しました。また、プログラム全体をに縛られないように分離しましたSession。ここで不足している部分はすべて次のようになります。

class UserRepository : IUserRepository
{
    public User FetchValidUser (string username, string password)
    {
        var user = db.FetchUser (username);

        if (user.Password != password)
            throw new Exception ("invalid password");

        return user;
    }
}

class RoleRepository : IRoleRepository
{
    public void CheckUserForRole (User user, string role)
    {
        var roles = db.FetchRoles (user);

        if (! roles.Contains (role))
            throw new Exception ("not a member");
    }
}

class SessionStorage : ILocalStorage
{
    public void StoreValue (string key, object value)
    {
        Session[key] = value;
    }
}

リファクタリングすることにより、一度にいくつかのことを達成しました。このプログラムは、基になる構造を引き裂く方法(NoSQLのデータベースレイヤーを捨てることができます)、またはSessionスレッドセーフでないなどのことがわかったらシームレスにロックを追加する方法をよりサポートしています。また、これら3つの依存関係を記述するための非常に簡単なテストを自分で行いました。

お役に立てれば :)


13

私は現在、主に〜10のブール値といくつかのintを取り、これに基づいてintの結果を与える「プロシージャ」関数をコーディングしています。

プロシージャ関数のそれぞれが決定論的であると確信しているため、指定された入力値のセットごとに特定のint結果を返します。理想的には、特定の入力値のセットに対してどのような結果を受け取るべきかを把握できる機能仕様が必要です。それがない場合は、特定の入力値のセットに対して(正常に動作すると推定される)rubyコードを実行し、結果を記録できます。次に、結果をテストにハードコードする必要があります。このテストは、コードが実際に正しいことわかっている結果を生成することの証明になるはずです。


既存のコードを実行し、結果を記録するために+1。この状況では、おそらく実用的なアプローチでしょう。
MarkJ

12

他の誰も実際の例を提供していないようです:

    public void testRoman() {
        RomanNumeral numeral = new RomanNumeral();
        assert( numeral.toRoman(1) == "I" )
        assert( numeral.toRoman(4) == "IV" )
        assert( numeral.toRoman(5) == "V" )
        assert( numeral.toRoman(9) == "IX" )
        assert( numeral.toRoman(10) == "X" )
    }
    public void testSqrt() {
        assert( sqrt(4) == 2 )
        assert( sqrt(9) == 3 )
    }

あなたは言う:

必要な結果を手動で計算してテストする必要があるため、このテストはまったく役に立たないと感じています

しかし、ポイントは、コーディング時よりも手動計算時の方が、ミスを犯す可能性がはるかに低い(または少なくともミスに気付く可能性が高い)ことです。

10進数からローマ数字への変換コードを間違える可能性はどのくらいですか?可能性が高い。手動で小数をローマ数字に変換するときに、間違いを犯す可能性はどのくらいありますか?あまりありません。それが、手動計算に対してテストする理由です。

平方根関数を実装するときに、間違いを犯す可能性はどのくらいありますか?可能性が高い。平方根を手動で計算するときに、間違いを犯す可能性はどのくらいありますか?おそらくより可能性が高い。しかし、sqrtを使用すると、計算機を使用して答えを得ることができます。

参考までに、私は主に〜10のブール値といくつかのintを受け取り、これに基づいてintの結果を与える「プロシージャ」関数を主にコーディングしています。テスト

そこで、ここで何が起こっているのかを推測します。関数はやや複雑なので、入力から出力がどうあるべきかを把握するのは困難です。そのためには、出力を理解するために関数を(頭の中で)手動で実行する必要があります。当然、それはちょっと役に立たず、エラーが発生しやすいようです。

重要なのは、正しい出力を見つけたいということです。ただし、これらの出力を正しいとわかっているものに対してテストする必要があります。それを計算するために独自のアルゴリズムを書くのは良くありません。この場合、値を手動で計算するのは難しすぎます。

ルビーコードに戻り、これらの元の関数をさまざまなパラメーターで実行します。ルビーコードの結果を取得し、単体テストに入れます。そうすれば、手動で計算する必要はありません。しかし、元のコードに対してテストしています。これは結果を同じに保つのに役立つはずですが、オリジナルにバグがある場合は役に立ちません。基本的に、元のコードをsqrtの例の計算機のように扱うことができます。

移植している実際のコードを示した場合、問題へのアプローチ方法に関するより詳細なフィードバックを提供できます。


また、Rubyコードにバグがあり、それが新しいコードにないことを知らない場合、Rubyの出力に基づいてコードがユニットテストに失敗すると、失敗した理由の調査は最終的にあなたを立証し、潜在的なRubyバグが見つかりました。とてもクールです。
アダムヴエル

11

私ができる唯一の単体テストは、単にテストでアルゴリズムを再コーディングすることだと思う

あなたはそのような単純なクラスに対してほぼ正しいです。

より複雑な計算機で試してみてください。ボウリングスコア計算機のように。

単体テストの価値は、テストするシナリオが異なる複雑な「ビジネス」ルールがある場合に、より簡単にわかります。

ミル計算機の実行をテストするべきではないと言っているわけではありません(計算機のアカウントは、1/3のような値を表すことができません。ゼロで除算するとどうなりますか?)より多くのブランチを使用して何かをテストし、カバレッジを取得する場合は、より明確に評価してください。


4
+1は、複雑な機能の場合に便利になります。adder.add()を浮動小数点値に拡張することにした場合はどうなりますか?行列?Legerアカウントの値?
joshin4colours

6

約100%のコードカバレッジに関する宗教的な熱心さにもかかわらず、すべてのメソッドをユニットテストする必要があるとは言えません。意味のあるビジネスロジックを含む機能のみ。単に数字を追加する関数は、テストする意味がありません。

私は現在、主に〜10個のブール値といくつかのintを取り、これに基づいてintの結果を与える「プロシージャ」関数をコーディングしています

あなたの本当の問題がそこにあります。ユニットテストが不自然に難しいまたは無意味であると思われる場合は、おそらく設計上の欠陥が原因です。オブジェクト指向であれば、メソッドシグネチャはそれほど大きくなく、テストする入力も少なくなります。

私はオブジェクト指向に行く必要はありません、手続き型プログラミングよりも優れています...


この場合、メソッドの「シグネチャ」は大規模ではなく、クラスメンバーであるstd :: vector <bool>から読み取ります。また、私は(おそらく設計が不十分な)ルビーコード(私が作成しなかった)を移植していることを明確にすべきでした
lezebulon

2
@lezebulon単一のメソッドが受け入れる入力が多数ある場合でも、そのメソッドはあまりにも多くの処理を実行します。
maple_shaft

3

私の観点では、ユニットテストは小さな加算クラスでさえも有用です。アルゴリズムを「再コーディング」することは考えず、ブラックボックスと考えないでください。高速乗算では、「a * b」)とパブリックインターフェイスを使用するよりも高速ですが、より複雑な試みを知っています。「地獄は何が間違っているのだろうか?」

ほとんどの場合、境界で発生します(これらのパターン++、-、+-、00を追加してテストを行い、-+、0 +、0-、+ 0、-0でこれらを完了する時間を確認します)。MAX_INTとMIN_INTで加算または減算(ネガティブの追加;))が行われた場合にどうなるかを考えてください。または、ゼロ付近で起こることをテストが非常に正確に見えるようにしてください。

すべての秘密はすべて、非常に単純です(複雑なクラスの場合もあります;)):単純なクラスの場合:クラスの契約について考え(契約による設計を参照)、それからテストします。あなたのinv、pre、postを知っているほど、テストが「完成」します。

テストクラスのヒント:メソッドには1つのアサートのみを記述してください。コードの変更後にテストが失敗したときに最良のフィードバックを得るために、メソッドに適切な名前(「testAddingToMaxInt」、「testAddingTwoNegatives」など)を付けます。


2

手動で計算された戻り値をテストしたり、テストでロジックを複製して期待される戻り値を計算するのではなく、期待されるプロパティの戻り値をテストします。

たとえば、行列を反転するメソッドをテストする場合、入力値を手動で反転したくない場合は、戻り値に入力を掛けて、単位行列を取得することを確認する必要があります。

このアプローチをメソッドに適用するには、その目的とセマンティクスを考慮して、戻り値が入力に関連するプロパティを識別する必要があります。


2

単体テストは生産性ツールです。変更要求を受け取り、それを実装し、ユニットテストgambitを介してコードを実行します。この自動テストにより時間を節約できます。

I feel that this test is totally useless, because you are required to manually compute the wanted result and test it, I feel like a better unit test here would be

論点。この例のテストは、クラスをインスタンス化し、一連のテストを実行する方法を示しています。単一の実装の詳細に集中することは、木の森が欠けていることです。

Can someone provide me with an example where unit testing is actually useful?

従業員エンティティがあります。エンティティには名前と住所が含まれます。クライアントはReportsToフィールドを追加することにしました。

void TestBusinessLayer()
{
   int employeeID = 1234
   Employee employee = Employee.GetEmployee(employeeID)
   BusinessLayer bl = new BusinessLayer()
   Assert.isTrue(bl.Add(employee))//assume Add returns true on pass
}

これは、従業員と働くためのBLの基本的なテストです。コードは、今行ったスキーマ変更の合否を示します。テストが行​​うのはアサーションだけではないことに注意してください。また、コードを実行することで、例外がバブルアップしないようにします。

時間が経つにつれて、テストを実施することで、一般的な変更が容易になります。コードは、例外とアサーションに対して自動的にテストされます。これにより、QAグループによる手動テストで発生するオーバーヘッドの多くが回避されます。UIの自動化は依然としてかなり困難ですが、アクセス修飾子を正しく使用すれば、他のレイヤーは一般的に非常に簡単です。

I feel like the only unit testing I could do would be to simply re-code the algorithm in the test.

手続き型ロジックでさえ、関数内に簡単にカプセル化されます。テストするint / primitive(またはモックオブジェクト)をカプセル化し、インスタンス化し、渡します。コードをコピーして単体テストに貼り付けないでください。それはDRYを打ち負かす。また、コードをテストするのではなく、コードのコピーをテストするため、テストが完全に無効になります。テストすべきコードが変更されても、テストは合格です!


<衒学> "色再現域"ではなく、 "策略" </衒学>。
チャオ

@chao lol毎日新しいことを学びます。
P.Brian.Mackey

2

あなたの例を取り上げて(少しリファクタリングして)、

assert(a + b, math.add(a, b));

助けにはならない:

  • math.add内部の動作を理解し、
  • エッジケースで何が起こるかを知っています。

それはほとんど言っているようです:

  • メソッドの機能を知りたい場合は、数百行のソースコードを自分で確認してください(はい、何百ものLOCを含むmath.add ことができます。以下を参照)。
  • メソッドが正しく機能するかどうかわからない。期待値と実際の値の両方が私が本当に期待したものと異なっていても問題ありませ

これは、次のようなテストを追加する必要がないことも意味します。

assert(3, math.add(1, 2));
assert(4, math.add(2, 2));

どちらも助けにはならず、少なくとも、最初のアサーションを作成すると、2番目のアサーションは何の有用性ももたらしません。

代わりに、何について:

const numeric Pi = 3.1415926535897932384626433832795;
const numeric Expected = 4.1415926535897932384626433832795;
assert(Expected, math.add(Pi, 1),
    "Adding an integer to a long numeric doesn't give a long numeric result.");
assert(Expected, math.add(1, Pi),
    "Adding a long numeric to an integer doesn't give a long numeric result.");

これは自明であり、あなたとソースコードを後で保守する人の両方にとって非常に役立ちます。この人がmath.addコードを単純化してパフォーマンスを最適化するためにわずかな変更を行い、次のようなテスト結果を見ると想像してください。

Test TestNumeric() failed on assertion 2, line 5: Adding a long numeric to an
integer doesn't give a long numeric result.

Expected value: 4.1415926535897932384626433832795
Actual value: 4

この人は、新しく変更されたメソッドが引数の順序に依存することをすぐに理解します。最初の引数が整数で、2番目の引数が長い数値の場合、結果は整数になりますが、長い数値が期待されます。

同様4.141592に、最初のアサーションでの実際の値を取得することは自明です。メソッドが大きな精度を処理することが期待されていることはわかっていますが、実際には失敗します。

同じ理由で、次の2つのアサーションが一部の言語で意味をなす場合があります。

// We don't expect a concatenation. `math` library is not intended for this.
assert(0, math.add("Hello", "World"));

// We expect the method to convert every string as if it was a decimal.
assert(5, math.add("0x2F", 5));

また、どうですか:

assert(numeric.Infinity, math.add(numeric.Infinity, 1));

自明です:メソッドが無限を正しく処理できるようにする必要があります。ゴーイング無限大を超えてまたは例外をスローすることは予想される動作ではありません。

それとも、あなたの言語に応じて、これはより理にかなっていますか?

/**
 * Ensures that when adding numbers which exceed the maximum value, the method
 * fails with OverflowException, instead of restarting at numeric.Minimum + 1.
 */
TestOverflow()
{
    UnitTest.ExpectException(ofType(OverflowException));

    numeric result = math.add(numeric.Maximum, 1));

    UnitTest.Fail("The tested code succeeded, while an OverflowException was
        expected.");
}

1

addのような非常に単純な関数の場合、テストは不要と考えることができますが、関数が複雑になるにつれて、テストが必要な理由がますます明らかになります。

プログラミング中に何をするかを考えてください(単体テストなし)。通常、コードを作成して実行し、動作することを確認してから、次の作業に進みますか?特に非常に大規模なシステム/ GUI /ウェブサイトでコードを記述すると、「実行して動作するかどうかを確認する」必要性がますます高まることがわかります。これを試して、それを試してください。その後、いくつかの変更を加え、同じことをもう一度やり直す必要があります。「実行して動作するかどうかを確認する」部分全体を自動化する単体テストを作成することで、時間を節約できることが非常に明らかになります。

プロジェクトがますます大きくなると、「実行して動作するかどうかを確認する」必要があるものの数が非現実的になります。したがって、GUI /プロジェクトのいくつかの主要なコンポーネントを実行して試してみて、他のすべてがうまくいくことを望みます。これは災害のレシピです。もちろん、文字通り数百人がGUIを使用している場合、人間として、クライアントが使用する可能性のあるあらゆる状況を繰り返しテストすることはできません。ユニットテストを実施している場合は、安定版を出荷する前に、または中央リポジトリ(職場で使用する場合)にコミットする前に、単純にテストを実行できます。また、後でバグが見つかった場合は、単体テストを追加するだけで将来的に確認できます。


1

単体テストを作成する利点の1つは、エッジケースを強制的に検討することで、より堅牢なコードを作成できることです。整数オーバーフロー、10進数の切り捨て、パラメーターのNULLの処理など、いくつかのエッジケースのテストについてはどうでしょうか。


0

たぶん、ADD命令でadd()が実装されたと仮定します。ジュニアプログラマーまたはハードウェアエンジニアがANDS / ORS / XORS、ビット反転、およびシフトを使用してadd()関数を再実装した場合、ADD命令に対してユニットテストを行うことができます。

一般に、add()のガット、またはテスト対象のユニットを乱数または出力ジェネレーターに置き換えた場合、何かが壊れていることをどのように知るのでしょうか?ユニットテストでその知識をエンコードします。壊れているかどうか誰にもわからない場合は、rand()のコードをチェックインして家に帰れば、仕事は完了です。


0

私はすべての返信の中でそれを逃したかもしれませんが、私にとって、ユニットテストの背後にある主な動機は、今日のメソッドの正確性を証明することではなく、それを変更するたびにそのメソッドの継続的な正確性を証明することです。

いくつかのコレクションのアイテムの数を返すような単純な関数を取ります。現在、リストがよく知っている1つの内部データ構造に基づいている場合、この方法は非常に痛々しいほど明白であるため、テストする必要はないと考えるかもしれません。その後、数か月または数年後に、あなた(または他の誰か)が内部リスト構造を置き換えることを決定します。getCount()が正しい値を返すことをまだ知る必要があります。

それがユニットテストが本当に独自に出てくるところです。

コードの内部実装を変更できますが、そのコードを使用するユーザーにとっては、結果は変わりません。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.