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使用 as 公共方法创建一个类DoStuff()并将其注入您的Tester类中。然后你有:

  • 一种DoStuff()方法,可通过测试获得
  • 一种DoSomeRandomStuff()可以用模拟测试而不是依赖于结果的方法DoStuff(),从而使其成为真正的单元测试。
  • 没有违反 SRP。
  • 总而言之,漂亮、清晰和可靠的代码。
于 2013-07-02T13:10:12.900 回答
3

您可以像这样使用 DEBUG 的预编译器指令(或创建您自己的指令):

#if DEBUG
    public void DoStuff(StuffToDo stuff)
#else
    private void DoStuff(StuffToDo stuff)
#endif
    {
        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");
        }
    }
于 2013-07-04T15:10:54.850 回答
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 回答
3

更新:鉴于此处的答案,似乎需要进行一些重要的说明。

  1. 枚举不会阻止您到达默认子句,这是到达它的有效方法: DoStuff((StuffToDo)9999)
  2. switch 语句中不需要 default 子句。在这种情况下,代码将在切换后默默地继续
  3. 如果方法是内部的,您可以使用 InternalsToVisible 直接对其进行单元测试
  4. 您可以重构代码,以便单元测试可以达到开关中处理的策略,就像@Morten 在他的回答中提到的那样
  5. 随机、日期和类似问题是单元测试的典型问题,您所做的就是更改代码,以便在单元测试期间可以替换这些代码。我的回答只是众多方法中的一种。我展示了如何通过字段成员注入它,但它可以通过构造函数注入,如果需要,它也可以是一个接口。
  6. 如果枚举不是私有的,我下面的代码示例可以改为公开 a 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");
        }
    }
}

附言。是的,它公开了 RandomStuffProvider,以便您可以在单元测试中使用。我没有成功,Func<StuffToDo>因为那是私人的。

于 2013-07-02T11:31:15.843 回答
2

我同意上面的内特。这可能是最简单的(StuffToDo.None)。作为替代方案,您也可以像这样重载该方法:

    private void DoStuff(StuffToDo stuff)
    {
        DoStuff(stuff.ToString());
    }

    private void DoStuff(string stuff)
        switch (stuff)
        {
            case "Swim":
                break;
            case "Bike":
                break;
            case "Run":
                break;
            default:
                throw new ArgumentOutOfRangeException("stuff");
        }
    }

然后,您可以实际测试您在默认语句中涵盖的情况。

于 2013-07-08T12:25:40.933 回答
1

这是无法访问的代码,因为 switch case 类型是 Enum,并且您已经编写了所有可能的情况。永远不会出现“默认”的情况。

您可以添加 'Unknown' 或一些不应有相应 case 语句的额外枚举值,然后此警告将消失。

private enum StuffToDo
{
    Swim = 0, 
    Bike,
    Run,
    // just for code coverage
    // will never reach here
    Unknown
}

代码覆盖率并没有深入评估每一种可能性,所以它不是测试它的问题。它根据预编码的预期行为匹配代码模式

Code Coverage 是这样认为的,你已经添加了现有枚举的所有可能性,没有办法调用 default。这就是他们的规则设置方式,您无法更改他们的规则。

于 2013-07-09T05:37:45.537 回答
1

您提到的“问题”是 switch...case 所固有的,因此您可以做的最好的事情是避免它依赖于不同的替代方案。我个人不喜欢太多的 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
}

使用第二个选项,您甚至可以将字典创建为全局变量并将其与枚举一起填充。

于 2013-07-07T18:05:16.327 回答
1

在单元测试用例中测试私有方法的一种方法是使用反射,但我觉得在大多数情况下可能会过度杀戮,因为它可以用其他方式进行测试。在您的代码中,要测试 switch 案例,您可以做的是,如果您的公共类方法将 StuffToDo 作为参数,那么您需要编写多个测试案例,每个案例都为 StuffToDo 传递一组不同的值。这样,当你的 switch 语句被执行时,你可以使用你的公共方法本身来验证行为,但在这种情况下,我再次假设你正在从你的公共方法获得输出。

查看您的代码我得到的另一种感觉是,您的公共方法没有接受任何输入,也没有给出任何内容,而是进行了看起来不正确的修改。看起来它默默地改变了可能令人困惑的事情。

尝试使用更具体的方法,清楚地说明它作为输入的内容以及如何修改它。

于 2013-06-26T15:26:15.583 回答
1

编写测试超出范围的值并期待异常。

于 2013-07-07T21:51:25.697 回答
0

我的 ReSharper 还“抱怨”无法访问的代码,这是我在编程时不喜欢的。叫我肛门,但我喜欢从看到死代码的那一刻起就摆脱它。

具体而言,对于您的情况,我会选择其中一种情况作为默认情况,并将其置于如下状态:

private void DoStuff(StuffToDo stuff)
{
    switch (stuff)
    {
        case StuffToDo.Swim:
            break;
        case StuffToDo.Bike:
            break;
        case StuffToDo.Run:
        default:
            break;
    }
}

由于您努力实现 100% 的测试覆盖率,理想情况下,当您添加一个应该区别对待的枚举条目时,这应该会中断。

如果你真的想确定,你仍然可以丑化它:

private void DoStuff(StuffToDo stuff)
{
    switch (stuff)
    {
        case StuffToDo.Swim:
            break;
        case StuffToDo.Bike:
            break;
        case StuffToDo.Run:
        default:
            if (stuff != StuffToDo.Run) throw new ArgumentOutOfRangeException("stuff");
            break;
    }
}
于 2013-07-09T06:12:00.277 回答