Search code examples
unit-testingnunitcode-coverageopencover

How would I get full OpenCover branch coverage for these code examples?


I want to test the following two (unrelated) methods and achieve full branch and statement coverage using OpenCover 2.0.802.1

public class Methods
{
    public static void MethodWithDelegate(SynchronizationContext context)
    {
        context.Send(delegate { Console.Beep(); }, null);
    }

    public static string MethodWithSwitchStatement(Type value)
    {
        string output = string.Empty;

        if (value != null)
        {
            switch (value.ToString())
            {
                case "System.Int32":
                    output = "int";
                    break;
                default:
                    output = "other type";
                    break;
            }
        }

        return output;
    }
}

I have written the following (NUnit) tests, one using a 'Moq' mock object:

[Test]
public void Test_ShouldCoverMethodWithDelegate()
{
    var context = new Mock<SynchronizationContext>();

    Methods.MethodWithDelegate(context.Object);

    context.Verify(x => x.Send(It.IsAny<SendOrPostCallback>(), It.IsAny<object>()));
}

[Test]
public void Test_ShouldCoverSwitchStatement()
{
    Assert.That(Methods.MethodWithSwitchStatement(null), Is.EqualTo(string.Empty));
    Assert.That(Methods.MethodWithSwitchStatement(typeof(int)), Is.EqualTo("int"));
    Assert.That(Methods.MethodWithSwitchStatement(typeof(float)), Is.EqualTo("other type"));
}

However, after running the tests through OpenCover, the coverage.xml file always contains a branch point with a zero visit count for both tests. The sequence coverage shows 100%.

Not being an IL expert, I'm not sure how I'd go about writing further tests to get the branch coverage to 100%.


Solution

  • Okay first lets look at the first method in IL (I am using IL SPY)

    .method public hidebysig static 
    void MethodWithDelegate (
        class [mscorlib]System.Threading.SynchronizationContext context
    ) cil managed 
    {
    // Method begins at RVA 0x2059
    // Code size 41 (0x29)
    .maxstack 8
    
    IL_0000: nop
    IL_0001: ldarg.0
    IL_0002: ldsfld class [mscorlib]System.Threading.SendOrPostCallback so8254847.Methods::'CS$<>9__CachedAnonymousMethodDelegate1'
    IL_0007: brtrue.s IL_001c
    
    IL_0009: ldnull
    IL_000a: ldftn void so8254847.Methods::'<MethodWithDelegate>b__0'(object)
    IL_0010: newobj instance void [mscorlib]System.Threading.SendOrPostCallback::.ctor(object, native int)
    IL_0015: stsfld class [mscorlib]System.Threading.SendOrPostCallback so8254847.Methods::'CS$<>9__CachedAnonymousMethodDelegate1'
    IL_001a: br.s IL_001c
    
    IL_001c: ldsfld class [mscorlib]System.Threading.SendOrPostCallback so8254847.Methods::'CS$<>9__CachedAnonymousMethodDelegate1'
    IL_0021: ldnull
    IL_0022: callvirt instance void [mscorlib]System.Threading.SynchronizationContext::Send(class [mscorlib]System.Threading.SendOrPostCallback, object)
    IL_0027: nop
    IL_0028: ret
    } // end of method Methods::MethodWithDelegate
    

    As you can see there is a conditional branch at IL_0007 which only gets executed if the cached anonymous delegate has been set otherwise it goes through the main code of setting up your delegate and then calling it.

    SOLUTION: Run the test twice - or forget about it as it is a bit of .NET optimization

    Now for the 2nd problem this time it is better to see what was actually produced in C# - you wrote switch statements but the compiler has used ifs instead

    public static string MethodWithSwitchStatement(Type value)
    {
        string output = string.Empty;
        if (value != null)
        {
            string a;
            if ((a = value.ToString()) != null && a == "System.Int32")
            {
                output = "int";
            }
            else
            {
                output = "other type";
            }
        }
        return output;
    }
    

    As you can see the compiler has introduced an if null test with the switch statement but because you already have this already then it will never be executed.

    SOLUTION: remove your initial if null test (as it is not needed).