如何从代码覆盖中测试或排除私有无法访问的代码

时间:2013-06-26 15:21:41

标签: c# unit-testing architecture code-coverage dotcover

我有一堆接近100%测试覆盖率的程序集,但我经常遇到如下例所示的情况。我无法测试默认的开关案例,这是为了防范未来的错误,我在枚举中添加了更多项目,但忘记更新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");
        }
    }
}

10 个答案:

答案 0 :(得分:9)

私有方法总是可以提取到自己的类中。特别是,如果他们持有复杂的逻辑,就像你的那样。我建议您使用StuffDoer作为公共方法创建DoStuff()课程,并将其注入Tester课程。然后你有:

  • DoStuff()方法,可通过测试
  • 可以使用模拟测试的DoSomeRandomStuff()方法,而不是依赖DoStuff()的结果,从而使其成为真正的单元测试。
  • 没有SRP违规。
  • 总而言之,漂亮,清晰和SOLID代码。

答案 1 :(得分: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");
        }
    }

答案 2 :(得分:3)

这样做:

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

这将确保它返回的数字是枚举的一部分,因此永远不会到达default:开关。 (Link

答案 3 :(得分:2)

更新:在这里给出答案,似乎需要做一些重要的澄清。

  1. 枚举不会阻止您达到默认子句,这是一种有效的方法: DoStuff((StuffToDo)9999)
  2. switch语句中不需要default子句。在这种情况下,代码将在切换
  3. 后静默地继续
  4. 如果方法是内部的,您可以使用InternalsToVisible直接对其进行单元测试
  5. 您可以通过单元测试达到交换机中处理的策略来重构代码,例如@Morten提及他的答案
  6. 随机,日期和类似是单元测试的典型问题,您所做的是以在单元测试期间可以替换它们的方式更改代码。我的答案只是众多方法的一种方式。我将展示如何通过字段成员注入它,但它可以通过构造函数注入,如果需要,它也可以是一个接口。
  7. 如果枚举不是私有的,我下面的代码示例可能会显示Func<StuffToDo>,这会使测试更具可读性。

  8. 你仍然可以这样做并达到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>因为那是私人的。

答案 4 :(得分:2)

我同意上面的Nate。这可能是最简单的(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");
        }
    }

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

答案 5 :(得分:1)

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

看看你的代码我得到的另一种感觉是,你的公共方法没有采取任何输入,也没有给出任何结果,但做了一些看起来不正确的修改。它看起来像是在默默地改变可能混淆的东西。

尝试使用更具体的方法,清楚地说明它所采取的输入以及如何修改它。

答案 6 :(得分:1)

您提到的“问题”是切换...案件的内在因素,因此您可以做的最好的事情就是依靠不同的选择。我个人不喜欢过多的开关......因为它缺乏灵活性。因此,这个答案旨在提供一种替代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
}

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

答案 7 :(得分:1)

编写用于测试超出范围值并期望异常的测试。

答案 8 :(得分:1)

它是无法访问的代码,因为交换机案例类型是Enum,并且您已编码所有可能的案例。永远不会出现“默认”的情况。

你可以添加'Unknown'或一些额外的enum值,这些值不应该有相应的case语句,然后这个警告就会消失。

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

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

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

答案 9 :(得分:0)

My 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;
        }
    }