24

テスト カバレッジがほぼ 100% のアセンブリが多数ありますが、以下の例のような状況に陥ることがよくあります。デフォルトの switch ケースをテストすることはできません。これは、列挙型にさらに項目を追加するが、新しい項目をサポートするために switch ステートメントを更新するのを忘れるという将来のバグを防ぐためにあります。

その「テストできない」コードを削除できるパターンを見つけてテストするか、そのコード行(ただしメソッド全体ではない) をカバレッジ分析から除外するようにマークしたいと考えています。

ばかげているように聞こえるかもしれませんが、デフォルトのケースが決して起こらないと仮定したくありません。また、デフォルトのケースを既存のものとバンドルしたくありません。そのようなバグを作成したときに例外がスローされるようにします。それは遅かれ早かれ起こるでしょう。

現時点では、 DotCoverを使用してカバレッジを計算しています。

注:これは単なるコード例ですが、かなり一般的なパターンを示していると思います。

public class Tester
{
    private enum StuffToDo
    {
        Swim = 0, 
        Bike,
        Run
    }

    public void DoSomeRandomStuff()
    {
        var random = new Random();
        DoStuff((StuffToDo)random.Next(3));
    }

    private void DoStuff(StuffToDo stuff)
    {
        switch (stuff)
        {
            case StuffToDo.Swim:
                break;
            case StuffToDo.Bike:
                break;
            case StuffToDo.Run:
                break;
            default:
                // How do I test or exclude this line from coverage?
                throw new ArgumentOutOfRangeException("stuff");
        }
    }
}
4

10 に答える 10

10

プライベート メソッドは、常に独自のクラスに抽出される候補です。特に、あなたのように複雑なロジックを保持している場合。StuffDoerを public メソッドとしてクラスを作成し、それをクラスDoStuff()に注入することをお勧めしますTester。次に、次のようになります。

  • DoStuff()テストによって到達可能なメソッド
  • DoSomeRandomStuff()結果に依存するのではなく、モックでテストできるメソッドDoStuff()。したがって、真の単体テストになります。
  • SRP 違反なし。
  • 全体として、素晴らしく、鮮明で、堅実なコードです。
于 2013-07-02T13:10:12.900 に答える
3

更新:ここでの回答を考えると、いくつかの重要な説明が必要なようです。

  1. 列挙型はデフォルト句に到達することを妨げません。これはそれに到達する有効な方法です: DoStuff((StuffToDo)9999)
  2. switch ステートメントでは default 句は必要ありません。その場合、コードは切り替え後に黙って続行します
  3. メソッドが内部の場合、InternalsToVisible を使用して直接単体テストできます。
  4. @Mortenが彼の答えに言及しているように、スイッチで処理される戦略に単体テストが到達できるようにコードをリファクタリングできます
  5. ランダム、日付などは単体テストの典型的な問題であり、単体テスト中にそれらを置き換えることができるようにコードを変更することです。私の答えは、そうするための多くの方法のうちの 1 つの方法にすぎません。フィールドメンバーを介して注入する方法を示しましたが、コンストラクターを介して注入することも、必要に応じてインターフェイスにすることもできます。
  6. 列挙型が非公開でない場合、以下のコード サンプルで代わりに を公開できますFunc<StuffToDo>。これにより、テストが少し読みやすくなります。

これを実行して 100% にすることもできます。

public class Tester
{
    private enum StuffToDo
    {
        Swim = 0, 
        Bike,
        Run
    }
    public Func<int> randomStuffProvider = GetRandomStuffInt;

    public void DoSomeRandomStuff()
    {
        DoStuff((StuffToDo)randomStuffProvider());
    }
    private static int GetRandomStuffInt()
    {
        //note: don't do this, you're supposed to reuse the random instance
        var random = new Random();
        return random.Next(3);
    }

    private void DoStuff(StuffToDo stuff)
    {
        switch (stuff)
        {
            case StuffToDo.Swim:
                break;
            case StuffToDo.Bike:
                break;
            case StuffToDo.Run:
                break;
            default:
                // How do I test or exclude this line from coverage?
                throw new ArgumentOutOfRangeException("stuff");
        }
    }
}

ps。はい、単体テストで使用できるように RandomStuffProvider を公開します。Func<StuffToDo>プライベートなので入れませんでした。

于 2013-07-02T11:31:15.843 に答える
3

これを行うだけです:

private static int GetRandomStuffInt()
{
    var random = new Random();
    DoStuff((StuffToDo)random.Next(Enum.GetNames(typeof(StuffToDo)).Length));
}

これにより、返される数値が列挙型の一部であることが保証されるため、default:スイッチに到達することさえありません。(リンク)

于 2013-07-07T20:41:40.840 に答える
1

範囲外の値をテストし、例外を予期するテストを作成します。

于 2013-07-07T21:51:25.697 に答える
1

あなたが参照する「問題」は switch...case に固有のものであるため、それを回避するためにできる最善のことは、別の代替手段に頼ることです。私は個人的にはあまり好きではありません...その柔軟性がないためです。したがって、この回答は、探している種類のゼロ不確実性を提供できる switch ステートメントの代替手段を提供することを目的としています。

この種の状況では、通常、関数の開始時に入力された配列から取得した一連の条件に依存します。これにより、不明な入力が自動的にフィルター処理されます。例:

private void DoStuffMyWay(StuffToDo stuff)
{
    StuffToDo[] accountedStuff = new StuffToDo[] { StuffToDo.Bike, StuffToDo.Run, StuffToDo.Swim };

    if (accountedStuff.Contains(stuff))
    {
        if (stuff == accountedStuff[0])
        {
            //do bike stuff
        }
        else if (stuff == accountedStuff[1])
        {
            //do run stuff
        }
        else if (stuff == accountedStuff[2])
        {
            //do swim stuff
        }
    }
}

これは確かにあまりエレガントではありませんが、私はエレガントなプログラマーではないと思います。

おそらく、問題に対して別のタイプのアプローチを好む限り、ここには別の選択肢があります。効率は劣りますが、見栄えが良くなります。

private void DoStuffWithStyle(StuffToDo stuff)
{
    Dictionary<StuffToDo, Action> accountedStuff = new Dictionary<StuffToDo, Action>();
    accountedStuff.Add(StuffToDo.Bike, actionBike);
    accountedStuff.Add(StuffToDo.Run, actionRun);
    accountedStuff.Add(StuffToDo.Swim, actionSwim);

    if (accountedStuff.ContainsKey(stuff))
    {
        accountedStuff[stuff].Invoke();
    }
}

private void actionBike()
{
    //do bike stuff
}

private void actionRun()
{
    //do run stuff
}

private void actionSwim()
{
   //do swim stuff
}

この 2 番目のオプションを使用すると、ディクショナリをグローバル変数として作成し、列挙型と共に入力することもできます。

于 2013-07-07T18:05:16.327 に答える