28

私たちはほとんどのビジネス ロジックの単体テストを行っていますが、大規模なサービス タスクとインポート/エクスポート ルーチンのいくつかをテストする最善の方法に行き詰まっています。たとえば、あるシステムからサードパーティ システムへの給与データのエクスポートを考えてみましょう。会社が必要とする形式でデータをエクスポートするには、最大 40 個のテーブルにヒットする必要があり、テスト データを作成して依存関係をモックアウトするという悪夢のような状況が発生します。

たとえば、次のことを考えてみましょう (約 3500 行のエクスポート コードのサブセット)。

public void ExportPaychecks()
{
   var pays = _pays.GetPaysForCurrentDate();
   foreach (PayObject pay in pays)
   {
      WriteHeaderRow(pay);
      if (pay.IsFirstCheck)
      {
         WriteDetailRowType1(pay);
      }
   }
}

private void WriteHeaderRow(PayObject pay)
{
   //do lots more stuff
}

private void WriteDetailRowType1(PayObject pay)
{
   //do lots more stuff
}

この特定のエクスポート クラスには、ExportPaychecks() というパブリック メソッドしかありません。これは、このクラスを呼び出す人にとって本当に意味のある唯一のアクションです...他のすべてはプライベートです(〜80のプライベート関数)。テストのためにそれらを公開することもできますが、それぞれを個別にテストするためにそれらをモックする必要があります (つまり、WriteHeaderRow 関数をモックしないと、ExportPaychecks を単独でテストすることはできません。これも大きな苦痛です。

これは単一のベンダーの単一のエクスポートであるため、ロジックをドメインに移動することは意味がありません。ロジックは、この特定のクラスの外ではドメインの意味を持ちません。テストとして、100% に近いコード カバレッジを持つ単体テストを構築しました ... しかし、これには、スタブ/モック オブジェクトに入力された非常に多くのテスト データに加えて、多くの依存関係をスタブ/モック化するために 7000 行を超えるコードが必要でした.

HRIS ソフトウェアのメーカーとして、何百もの輸出入を行っています。他の会社は本当にこの種の単体テストを行っていますか? もしそうなら、痛みを軽減する近道はありますか? 私は、「インポート/エクスポート ルーチンのユニット テストは行わない」と言って、後で統合テストを実装するだけの誘惑にかられます。

更新- すべての回答に感謝します。コードを混乱させることなく、大きなファイルのエクスポートのようなものを簡単にテスト可能なコードのブロックに変換する方法をまだ見ていないので、私が見たいのは例です。

4

10 に答える 10

18

単一の公開メソッドを使用して巨大なコード ベース全体をカバーしようとするこの (試みられた) 単体テストのスタイルは、小さな開口部から複雑な操作を実行する外科医、歯科医、または婦人科医を常に思い出させます。可能ですが、簡単ではありません。

カプセル化はオブジェクト指向設計の古い概念ですが、テスト容易性が損なわれるほど極端にカプセル化を採用する人もいます。Open/Closed Principleと呼ばれる別の OO 原則があり、これはテスト容易性により適しています。カプセル化は依然として価値がありますが、拡張性を犠牲にしているわけではありません。実際、テスト容易性は、 Open/Closed Principle の別の言葉です

プライベート メソッドを公開する必要があると言っているわけではありませんが、アプリケーションを構成可能なパーツ (1 つの大きなTransaction Scriptではなく、連携する多数の小さなクラス) にリファクタリングすることを検討する必要があるということです。単一のベンダーに対するソリューションのためにこれを行うのはあまり意味がないと思うかもしれませんが、現在あなたは苦しんでおり、これが 1 つの解決策です。

複雑な API で 1 つのメソッドを分割すると、多くの柔軟性が得られることがよくあります。1 回限りのプロジェクトとして始まったものが、再利用可能なライブラリに変わる可能性があります。


目前の問題に対してリファクタリングを実行する方法について、いくつかの考えを次に示します。すべての ETL アプリケーションは、少なくとも次の 3 つの手順を実行する必要があります。

  1. ソースからデータを抽出する
  2. データを変換する
  3. データを宛先にロードする

(したがって、ETLという名前です)。リファクタリングの開始として、これにより、明確な責任を持つ少なくとも 3 つのクラスが得られます: ExtractorTransformerおよびLoader. 今では、1 つの大きなクラスの代わりに、より的を絞った責任を持つ 3 つのクラスがあります。それについて面倒なことは何もなく、すでにもう少しテスト可能です。

次に、これら 3 つの領域のそれぞれにズームインして、責任をさらに分割できる場所を確認します。

  • 少なくとも、ソース データの各「行」の適切なメモリ内表現が必要になります。ソースがリレーショナル データベースの場合は、ORM を使用することをお勧めしますが、そうでない場合は、そのようなクラスをモデル化して、各行の不変条件を正しく保護する必要があります (たとえば、フィールドが null 非許容の場合、クラスは保証する必要があります)。これは、null 値が試行された場合に例外をスローすることによって行われます)。このようなクラスには明確に定義された目的があり、分離してテストできます。
  • 同じことが目的地にも当てはまります。そのためには、優れたオブジェクト モデルが必要です。
  • ソースで高度なアプリケーション側のフィルタリングが行われている場合は、仕様設計パターンを使用してこれらを実装することを検討できます。それらも非常にテストしやすい傾向があります。
  • 変換ステップは、多くのアクションが発生する場所ですが、変換元と変換先の両方の適切なオブジェクト モデルが得られたので、マッパー(再びテスト可能なクラス) によって変換を実行できます。

ソース データと宛先データの「行」が多数ある場合は、論理的な「行」ごとにマッパーでこれをさらに分割できます。

煩雑になる必要はありません。追加の利点 (自動テスト以外) は、オブジェクト モデルがより柔軟になったことです。2 つの側面のいずれかを含む別のETL アプリケーションを作成する必要がある場合、コードの少なくとも 3 分の 1 はすでに作成されています。

于 2010-01-18T08:38:05.313 に答える
6

最初に必要なのは統合テストです。これらは、関数が期待どおりに実行されることをテストし、これについて実際のデータベースにアクセスできます。

節約できるネットができたら、コードをリファクタリングして保守しやすくし、単体テストを導入することができます。

serbrech Workign Effectly with Legacy codeで述べたように、終わりがないので、グリーンフィールドプロジェクトでも読むことを強くお勧めします。

http://www.amazon.com/Working-Effectively-Legacy-Michael-Feathers/dp/0131177052

私が尋ねる主な質問は、コードがどのくらいの頻度で変更されるかということです。まれな場合は、単体テストを導入するために努力する価値があります。頻繁に変更される場合は、少しクリーンアップすることを検討します。

于 2010-01-16T23:11:02.237 に答える
4

統合テストで十分かもしれないようです。特に、これらのエクスポートルーチンが一度実行されると変更されない場合、または限られた時間しか使用されない場合。バリエーションのあるサンプル入力データを取得し、最終結果が期待どおりであることを確認するテストを行います。

テストに関する懸念は、作成する必要のある偽のデータの量でした。共有フィクスチャ( http://xunitpatterns.com/Shared%20Fixture.html )を作成することで、これを減らすことができる場合があります。単体テストの場合、エクスポートするビジネスオブジェクトのメモリ内表現である可能性のあるフィクスチャ、または統合テストの場合、既知のデータで初期化された実際のデータベースである可能性があります。重要なのは、共有フィクスチャを生成する方法は各テストで同じであるため、新しいテストを作成するには、既存のフィクスチャを微調整して、テストするコードをトリガーするだけです。

では、統合テストを使用する必要がありますか?1つの障壁は、共有フィクスチャを設定する方法です。データベースをどこかに複製できる場合は、DbUnitなどを使用して共有フィクスチャを準備できます。コードを細かく分割する(インポート、変換、エクスポート)方が簡単な場合があります。次に、DbUnitベースのテストを使用してインポートとエクスポートをテストし、通常の単体テストを使用して変換ステップを検証します。その場合、変換ステップの共有フィクスチャを設定するためにDbUnitは必要ありません。コードを3つのステップ(抽出、変換、エクスポート)に分割できる場合は、少なくとも、バグや後で変更される可能性のある部分にテスト作業を集中させることができます。

于 2010-01-20T21:21:45.647 に答える
3

私は C# とは何の関係もありませんが、ここで試すことができるアイデアがいくつかあります。コードを少し分割すると、基本的にシーケンスに対して実行される一連の操作であることがわかります。

最初のものは現在の日付の支払いを受け取ります:

    var pays = _pays.GetPaysForCurrentDate();

2番目のものは無条件に結果を処理します

    foreach (PayObject pay in pays)
    {
       WriteHeaderRow(pay);
    }

3 つ目は条件付き処理を実行します。

    foreach (PayObject pay in pays)
    {
       if (pay.IsFirstCheck)
       {
          WriteDetailRowType1(pay);
       }
    }

これで、これらのステージをより一般的にすることができます (疑似コードで申し訳ありません。C# はわかりません)。

    var all_pays = _pays.GetAll();

    var pwcdate = filter_pays(all_pays, current_date()) // filter_pays could also be made more generic, able to filter any sequence

    var pwcdate_ann =  annotate_with_header_row(pwcdate);       

    var pwcdate_ann_fc =  filter_first_check_only(pwcdate_annotated);  

    var pwcdate_ann_fc_ann =  annotate_with_detail_row(pwcdate_ann_fc);   // this could be made more generic, able to annotate with arbitrary row passed as parameter

    (Etc.)

ご覧のように、個別にテストしてから任意の順序で接続できる未接続のステージのセットができました。このような接続または構成は、個別にテストすることもできます。など(つまり、何をテストするかを選択できます)

于 2010-01-16T08:32:27.293 に答える
2

トマシュ・ジエリンスキーがその答えの一部を持っていると思います。しかし、3500行の手続き型コードがあるとすると、問題はそれよりも大きくなります。それをより多くの関数に分割しても、テストには役立ちません。ただし、これは、別のクラスにさらに抽出できる責任を特定するための最初のステップです(メソッドに適切な名前がある場合は、それが明らかな場合もあります)。

このようなクラスでは、このクラスをテストにインスタンス化できるようにするためだけに取り組むべき依存関係の驚くべきリストがあると思います。その場合、テストでそのクラスのインスタンスを作成することは非常に困難になります...MichaelFeathersの本「WorkingWithLegacyCode」は、そのような質問に非常によく答えます。そのコードをうまくテストできるようにするための最初の目標は、クラスの役割を識別し、それをより小さなクラスに分割することです。もちろん、それは簡単に言うことができ、皮肉なことに、変更を保護するためのテストなしで行うのは危険です...

そのクラスにはパブリックメソッドが1つしかないということです。これにより、すべてのプライベートメソッドのユーザーについて心配する必要がなくなるため、リファクタリングが容易になります。カプセル化は素晴らしいですが、そのクラスにプライベートなものがたくさんある場合、それはおそらくここに属していないことを意味し、最終的にテストできるように、そのモンスターからさまざまなクラスを抽出する必要があります。少しずつ、デザインはよりきれいに見えるはずです、そしてあなたはその大きなコードのより多くをテストすることができるでしょう。これを開始する場合の親友はリファクタリングツールになり、クラスとメソッドを抽出するときにロジックを壊さないようにするのに役立つはずです。

繰り返しになりますが、マイケルフェザーズの本は必読のようです:) http://www.amazon.com/Working-Effectively-Legacy-Michael-Feathers/dp/0131177052

追加例:

この例はMichaelFeathersの本からのものであり、私が思うにあなたの問題をよく示しています。

RuleParser  
public evaluate(string)  
private brachingExpression  
private causalExpression  
private variableExpression  
private valueExpression  
private nextTerm()  
private hasMoreTerms()   
public addVariables()  

ここでは明らかですが、メソッドnextTermとhasMoreTermsを公開することは意味がありません。これらのメソッドは誰にも見られないはずです。次の項目に移動する方法は、間違いなくクラスの内部にあります。では、このロジックをテストする方法は??

これが別の責任であることがわかり、クラス、たとえばTokenizerを抽出する場合。このメソッドは、この新しいクラス内で突然公開されます。それがその目的だからです。そうすれば、その動作をテストするのが簡単になります...

したがって、それを巨大なコードに適用し、その一部を責任の少ない他のクラスに抽出し、これらのメソッドを公開する方が自然であると感じる場合は、それらを簡単にテストすることもできます。あなたはそれらをマップするために約40の異なるテーブルにアクセスしていると言いました。それをマッピングの各部分のクラスに分割してみませんか?

私が読めないコードについて推論するのは少し難しいです。これを行うのを妨げる他の問題があるかもしれませんが、それは私の最善の試みです。

これが幸運に役立つことを願っています:)

于 2010-01-16T15:22:29.130 に答える
2

これは、すべてをモックするという概念が通用しない領域の 1 つです。確かに、各メソッドを個別にテストすることは、物事を行うための「より良い」方法ですが、すべてのメソッドのテスト バージョンを作成する労力と、コードをテスト データベースに向ける労力を比較してください (必要に応じて、各テスト実行の開始時にリセットします)。 )。

これは、コンポーネント間に多くの複雑な相互作用があるコードで私が使用しているアプローチであり、十分に機能します。各テストでより多くのコードが実行されるため、問題が発生した場所を正確に見つけるためにデバッガーを使用する必要が生じる可能性が高くなります。 .

于 2010-01-25T02:21:38.887 に答える
2

複数の ~3.5 Klines データ エクスポート機能があり、それらの間に共通の機能がまったくないことを受け入れるのは本当に難しいと思います。もしそうなら、ユニットテストはここで見る必要があるものではないかもしれません。各エクスポート モジュールが実際に行うことが 1 つだけであり、それが本質的に不可分である場合は、スナップショット比較、データ駆動型統合テスト スイートが必要になる可能性があります。

共通の機能がある場合は、それぞれを (個別のクラスとして) 抽出し、個別にテストします。これらの小さなヘルパー クラスは当然、さまざまなパブリック インターフェイスを持ちます。これにより、テストできないプライベート API の問題が軽減されます。

実際の出力形式がどのように見えるかについての詳細は提供しませんが、それらが一般的に表形式、固定幅、または区切りテキストである場合、少なくともエクスポーターを構造コードとフォーマットコードに分割できるはずです。つまり、上記のコード例の代わりに、次のようなものがあります。

public void ExportPaychecks(HeaderFormatter h, CheckRowFormatter f)
{
   var pays = _pays.GetPaysForCurrentDate();
   foreach (PayObject pay in pays)
   {
      h.formatHeader(pay);
      f.WriteDetailRow(pay);
   }
}

HeaderFormatterおよび抽象クラスはCheckRowFormatter、これらのタイプのレポート要素に共通のインターフェイスを定義し、個々の具象サブクラス (さまざまなレポート用) には、たとえば (または特定のベンダーが必要とするものは何でも) 重複行を削除するためのロジックが含まれます。

これを切り分けるもう 1 つの方法は、データの抽出とフォーマットを互いに分離することです。さまざまなデータベースから必要な表現のスーパーセットである中間表現にすべてのレコードを抽出するコードを記述し、次に、uber-format から各ベンダーに必要な形式に変換する比較的単純なフィルター ルーチンを記述します。


これについてもう少し考えてみると、これが ETL アプリケーションであることがわかりましたが、あなたの例は 3 つのステップすべてを組み合わせているようです。これは、最初のステップは、すべてのデータが最初に抽出され、次に翻訳され、保存されるように物事を分割することであることを示唆しています. 少なくともこれらのステップを個別にテストできます。

于 2010-01-20T00:41:12.080 に答える
1

あなたが説明したものと同様のレポートをいくつか維持していますが、それらの数は多くなく、データベース テーブルも少なくなっています。私は、あなたに役立つように十分に拡張できる3つの戦略を使用します。

  1. メソッド レベルでは、主観的に「複雑」と思われるものはすべてユニット テストします。これには、バグ修正の 100% に加えて、私が緊張するものすべてが含まれます。

  2. モジュール レベルでは、主なユース ケースの単体テストを行います。あなたが遭遇したように、これは何らかの方法でデータをモックする必要があるため、かなり苦痛です. データベース インターフェイスを抽象化することでこれを実現しました (つまり、レポート モジュール内で直接 SQL 接続を行わないようにしました)。いくつかの単純なテストでは、テスト データを手動で入力しました。その他のテストでは、クエリを記録および/または再生するデータベース インターフェイスを作成して、実際のデータでテストをブートストラップできるようにしました。つまり、レコード モードで 1 回実行すると、実際のデータが取得されるだけでなく、スナップショットがファイルに保存されます。再生モードで実行すると、実際のデータベース テーブルではなく、このファイルが参照されます。(これを行うことができるモック フレームワークがあると確信していますが、私の世界のすべての SQL 相互作用には署名があるため、Stored Procedure Call -> Recordset自分で書くだけでとても簡単でした。)

  3. 幸運なことに、運用データの完全なコピーを備えたステージング環境にアクセスできるので、以前のソフトウェア バージョンに対して完全な回帰を使用して統合テストを実行できます。

于 2010-01-16T14:48:37.783 に答える
0

Moqを調べましたか?

サイトからの引用:

Moq (「モックユー」または単に「モック」と発音) は、.NET 3.5 (つまり、Linq 式ツリー) と C# 3.0 の機能 (つまり、ラムダ式) を最大限に活用するためにゼロから開発された、.NET 用の唯一のモッキング ライブラリです。最も生産的で、タイプ セーフで、リファクタリングしやすいモッキング ライブラリです。

于 2010-01-16T06:32:50.703 に答える