グラフ構造を使用してどのようにコードを単体テストしますか?


18

私は、依存関係グラフをナビゲートし、依存関係のサイクルまたは矛盾を探す(再帰的)コードを書いています。ただし、これを単体テストする方法はわかりません。問題は、発生する可能性があるすべての興味深いグラフ構造のコードハンドルと、すべてのノードが適切に処理されるようにすることです。

通常、一部のコードが動作することを確信するには100%の回線またはブランチのカバレッジで十分ですが、100%のパスカバレッジであってもまだ疑問があるように感じます。

したがって、テストケースのグラフ構造を選択して、コードが実際のデータで見つかる可能性のあるすべての順列を処理できることを確信できるようにするにはどうすればよいでしょうか。


PS-問題がある場合、グラフ内のすべてのエッジに「必要」、「不可」のラベルが付けられ、自明なサイクルはなく、2つのノード間に1つのエッジしかありません。


PPS-この追加の問題ステートメントは、もともと質問の著者によって以下のコメントで投稿されました。

For all vertices N in forest F, for all vertices M, in F, such that if there are any walks between N and M they all must either use only edges labelled 'conflict' or 'requires'.


13
他のメソッドを単体テストするのと同じ方法。各メソッドのすべての「興味深い」テストケースを特定し、それらのユニットテストを記述します。あなたの場合、「興味深い」グラフ構造のそれぞれについて、依存関係の定型グラフを作成する必要があります。
ダンク14

@Dunkトリッキーなものはすべてカバーされていると考え続け、特定の構造がこれまで考えなかった問題を引き起こすことに気付きます。私たちが考えることができるすべてのトリッキーなテストは私たちがやっていることです、私が見つけたいと思っているのは、基本的な形の還元性などを使用して厄介な例を生成するためのいくつかのガイドライン/手順です
2014

6
それは、あらゆる形式のテストの問題です。知っているのは、あなたが考えたテストが機能するということだけです。テストに合格したからといって、swにエラーがないということではありません。どのプロジェクトにも同じ問題があります。現在のプロジェクトを提供する最終段階にあるため、製造を開始できます。今私たちが遭遇するエラーのタイプは、かなりあいまいになる傾向があります。たとえば、ハードウェアがまだ仕様どおりに機能しているが、ほとんど問題なく、同じ問題を抱えている他のハードウェアと組み合わせた場合、問題が発生します。だけ時々 :( SWが十分にテストされたが、我々はすべてのものを考えていなかった
ダンク

あなたが説明することは、ユニットテストではなく、統合テストに似ています。単体テストは、メソッドがグラフ内の円を見つけることができることを確認します。他の単体テストでは、特定のグラフの特定の円がテスト対象のクラスによって処理されることを確認します。
SpaceTrucker

サイクル検出はよくカバーされたトピックであり(Knuthおよび以下の回答も参照)、ソリューションには多数の特殊なケースは含まれないため、まず問題の原因を特定する必要があります。それはあなたが言及した矛盾によるものですか?その場合、それらに関する詳細情報が必要です。実装の選択の結果である場合、おそらく大きな方法でリファクタリングする必要があるかもしれません。基本的に、これはあなたがあなたの道を考えなければならない設計上の問題です、TDDは行き止まりの前に迷路の奥深くに連れて行くことができる間違ったアプローチです。
sdenham

回答:


5

私たちはすべてのトリッキーなものをカバーしていると考え続け、特定の構造がこれまで考えなかった問題を引き起こすことに気付きます。私たちが考えることができるすべてのトリッキーなテストは、私たちがやっていることです。

それは良いスタートのように思えます。すでにカバレッジベースのテストについて述べたように、境界値分析等価パーティション分割などの古典的な手法を既に適用しようとしていると思います。優れたテストケースの構築に多くの時間を費やした後、チーム、そしてテスター(もしあれば)がアイデアを使い果たすまでになります。そして、それはあなたがのパスおくべきポイントだユニットテストをしてすることができますように多くの実世界のデータとしてでテストを開始します。

実稼働データから多種多様なグラフを選択する必要があることは明らかです。プロセスのその部分だけのために、いくつかの追加のツールまたはプログラムを作成する必要があるかもしれません。ここで難しいのは、おそらく1万の異なる実世界のグラフをプログラムに入れたときに、プログラムの出力の正確さを検証することです。プログラムが常に正しい出力を生成するかどうかはどうすればわかりますか?明らかに手動でチェックすることはできません。したがって、運が良ければ、依存関係チェックの2番目の非常に単純な実装を行うことができます。これはパフォーマンスの期待を満たしていない可能性がありますが、元のアルゴリズムよりも簡単に検証できます。また、多くの妥当性チェックをプログラムに直接統合してみてください(たとえば、

最後に、すべてのテストがバグの存在を証明するだけで、バグがないことを証明できることを受け入れることを学びます。


5

1.ランダム化されたテスト生成

グラフを生成するアルゴリズムを作成し、数百(またはそれ以上)のランダムグラフを生成して、それぞれをアルゴリズムにスローします。

興味深い障害を引き起こすグラフのランダムシードを保持し、それらを単体テストとして追加します。

2.難しい部分をハードコードする

知っているグラフ構造の中には、すぐにコードを記述したり、それらを組み合わせてアルゴリズムにプッシュしたりするコードを書くのが難しいものがあります。

3.完全なリストを生成する

ただし、「実際のデータにある考えられるすべての順列をコードで処理できる」ことを確認したい場合は、ランダムシードからではなく、すべての順列を調べてこのデータを生成する必要があります。(これは、地下鉄の鉄道信号システムをテストするときに行われ、テストに時間がかかる膨大な量のケースを提供します。地下鉄の場合、システムは制限されているため、順列の数には上限があります。適用)


質問者は、彼らがすべてのケースを検討したかどうかを見分けることができないと書いています。これは、それらを列挙する方法がないことを意味します。彼らが問題領域を十分に理解するまで、テスト方法は重要な問題です。
sdenham

@sdenham文字通り、有効な組み合わせの数が無限にあるものをどのように列挙しますか?「これらは、実装のバグをキャッチすることが最も難しいグラフ構造です」という行に沿って何かを見つけたいと思っていました。ドメインは単純なので十分に理解していますFor all vertices N in forest F, for all vertices M, in F, such that if there are any walks between N and M they all must either use only edges labelled 'conflict' or 'requires'.。ドメインは問題ではありません。
そり

@ArtB:問題を明確にしてくれてありがとう。あなたが言ったように、任意の2つの頂点の間にエッジは1つしかなく、サイクルのあるパスを明らかに除外しています(または任意のサイクルの少なくとも1つ以上のパス)、そして少なくとも文字通り無限の数がないことを知っています可能な有効な組み合わせ、これは進行中です。すべての可能性を列挙する方法を知ることは、あなたがそれをしなければならないということと同じではないことに注意してください。もっと考えて
みよう...-sdenham

@ArtB:質問を修正して、ここで指定した問題ステートメントの更新を含める必要があります。また、これらが有向エッジである(それが当てはまる場合)こと、およびアルゴリズムが処理する必要がある状況だけでなく、サイクルがグラフのエラーと見なされるかどうかを述べると役立つ場合があります。
sdenham

4

この場合、十分なテストを行うことはできず、実世界の大量のデータやファジングでさえもできません。100%のコードカバレッジ、または100%のパスカバレッジでさえ、再帰関数をテストするには不十分です。

再帰関数は形式的な証明に耐える(この場合はそれほど難しくないはずです)か、そうではありません。コードがアプリケーション固有のコードと絡み合いすぎて副作用を排除できない場合は、そこから始めます。

アルゴリズム自体は、すべてのノードから実行される、訪問先ノードのリストと交差してはならないブラックリストを追加した、単純な広範な最初の検索に似た単純なフラッディングアルゴリズムのように聞こえます。

foreach nodes as node
    foreach nodes as tmp
        tmp.status = unmarked

    tovisit = []
    tovisit.push(node)
    node.status = required

    while |tovisit| > 0 do
        next = tovisit.pop()
        foreach next.requires as requirement
            if requirement.status = unmarked
                tovisit.push(requirement)
                requirement.status = required
            else if requirement.status = blacklisted
                return false
        foreach next.collides as collision
            if collision.status = unmarked
                requirement.status = blacklisted
            else if requirement.status = required
                return false
return true

この反復アルゴリズムは、任意のアーティファクトから開始し、開始アーティファクトが常に必要な任意のアーティファクトのグラフに対して、依存関係が不要であり、同時にブラックリストに登録されるという条件を満たします。

それはあなた自身の実装と同じくらい速いかもしれないし、そうでないかもしれないが、それはすべてのケースで終了することが証明できる(外側のループの各反復に関して、各要素はtovisitキューに一度だけプッシュできる)、それは到達可能な全体をあふれさせるグラフ(帰納的証拠)、および各ノードから開始して、アーティファクトを同時に要求し、ブラックリストに登録する必要があるすべてのケースを検出します。

独自の実装が同じ特性を持っていることを示すことができれば、ユニットテストを行うことなく正確性を証明できます。キューのプッシュとポップ、キューの長さのカウント、プロパティの繰り返しなどの基本的な方法のみをテストし、副作用がないことを示す必要があります。

編集:このアルゴリズムが証明しないのは、グラフにサイクルがないことです。ただし、有向非巡回グラフはよく研究されているトピックなので、この特性を証明する既製のアルゴリズムを見つけることも簡単です。

ご覧のとおり、車輪を再発明する必要はまったくありません。


3

「すべての興味深いグラフ構造」や「適切に処理された」などのフレーズを使用しています。これらのすべての構造に対してコードをテストし、コードがグラフを適切に処理するかどうかを判断する方法がない限り、テストカバレッジ分析などのツールのみを使用できます。

いくつかの興味深いグラフ構造を見つけてテストし、適切な処理が何であるかを判断し、コードがそれを行うことを確認することから始めることをお勧めします。次に、a)ルールに違反する壊れたグラフ、またはb)問題のあるあまり面白くないグラフにこれらのグラフを混乱させることができます。コードがそれらを適切に処理できないかどうかを確認します。


これはテストへの優れたアプローチですが、問題の中心的な問題であるすべてのケースを確実にカバーする方法は解決しません。それには、より多くの分析とリファクタリングが必要になると思います-上記の私の質問を参照してください。
sdenham


2

アルゴリズムをテストするのがこの種の困難になると、テストに基づいてアルゴリズムを構築するTDDに行きます。

要するにTDD、

  • テストを書く
  • 失敗しているのを見て
  • コードを修正する
  • すべてのテストに合格していることを確認してください
  • リファクタリング

サイクルを繰り返します

この特定の状況では、

  1. 最初のテストは、アルゴリズムがサイクルを返さないシングルノードグラフです
  2. 2つ目は、アルゴリズムがサイクルを返さない、サイクルのない3ノードグラフです。
  3. 次は、アルゴリズムがサイクルを返さないサイクルで3ノードグラフを使用することです
  4. 可能性に応じて、もう少し複雑なサイクルに対してテストできます

この方法の重要な側面の1つは、可能なステップ(可能性のあるシナリオを単純なテストに分割する)のテストを常に追加する必要があることです。

最後に、1つ以上の複雑な統合テストを追加して、予期しない問題(グラフが非常に大きい場合や再帰を使用する場合のスタックオーバーフローエラー/パフォーマンスエラーなど)があるかどうかを確認する必要があります。


2

最初に述べられ、Mackeの返信の下でコメントによって更新された問題の私の理解には、以下が含まれます。1)両方のエッジタイプ(依存関係および競合)が指示されます。2)2つのノードが1つのエッジで接続されている場合、別のタイプまたは逆であっても、別のノードで接続してはなりません。3)2つのノード間のパスが異なるタイプのエッジを混合することによって構築できる場合、それは無視される状況ではなくエラーです。4)1つのタイプのエッジを使用して2つのノード間にパスがある場合、他のタイプのエッジを使用して2つのノード間に別のパスがない場合があります。5)シングルエッジタイプまたは混合エッジタイプのサイクルは許可されません(アプリケーションでの推測から、競合のみのサイクルがエラーであるかどうかはわかりませんが、そうでない場合はこの条件を削除できます)。

さらに、使用されるデータ構造は、表現されるこれらの要件の違反を防止しないと仮定します(たとえば、ノードペアが常に、ノードペアから(タイプ、方向)へのマップで条件2に違反するグラフを表現できませんでした)特定のエラーを表現できない場合、考慮されるケースの数を減らします。

ここで考慮できるグラフは実際には3つあります。1つだけのエッジタイプの2つ、および2つのタイプのそれぞれの1つの結合によって形成される混合グラフです。これを使用して、いくつかのノードまでのすべてのグラフを体系的に生成できます。最初に、ノードの任意の2つの順序のペア(有向グラフであるため順序付けられたペア)の間に1つ以下のエッジを持つN個のノードのすべての可能なグラフを生成します。各ペアの結合を形成します。

データ構造が条件2の違反を表現できない場合、依存関係グラフのスペース内に収まる可能性のあるすべての競合グラフを構築するだけで、考慮されるケースを大幅に減らすことができます。それ以外の場合、結合の形成中に条件2の違反を検出できます。

最初のノードからの結合グラフの幅優先走査では、到達可能なすべてのノードへのすべてのパスのセットを構築でき、そのようにして、すべての条件の違反をチェックできます(サイクル検出では、Tarjanのアルゴリズムを使用します。)

他のノードからのパスは、他の場合には最初のノードからのパスとして表示されるため、グラフが切断されていても、最初のノードからのパスのみを考慮する必要があります。

エラー(条件3)ではなく、混合エッジパスを単純に無視できる場合は、依存関係グラフと競合グラフを個別に検討し、一方が到達可能な場合は他方に到達しないことを確認するだけで十分です。

N-1ノードのグラフの調査で見つかったパスを覚えている場合、Nノードのグラフを生成および評価するための開始点としてそれらを使用できます。

これは、ノード間で同じタイプの複数のエッジを生成しませんが、そうするために拡張できます。ただし、これによりケースの数が大幅に増加するため、テスト対象のコードでこのようなケースをすべて表現できないようにしたり、失敗した場合は、事前にそのようなケースをすべて除外しておくとよいでしょう。

このようなオラクルを書くための鍵は、それが非効率であることを意味する場合でも、できるだけシンプルに保ち、信頼を確立できるようにすることです(理想的には、テストによって裏付けられたその正当性の引数を通じて)

テストケースを生成する手段があり、作成したオラクルを信頼して、良いものと悪いものを正確に分離したら、これを使用してターゲットコードの自動テストを実行できます。それが実行可能でない場合、次の最良のオプションは、特徴的なケースの結果をくまなく調べることです。オラクルは、検出したエラーを分類し、各タイプのパスの数と長さ、両方のタイプのパスの開始点にノードがあるかどうかなど、受け入れられたケースに関する情報を提供します。あなたが前に見たことがないケースを探すのに役立つかもしれません。

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