3

I've got a simple method that calculate the total from a collection.

public void MethodToTest(Collection<int> collection)
{
    int sum = 0;
    foreach (int value in collection)
    {
        sum += value;
    }
}

The goal is to get a 100% at branch coverage using the opencoverage tool run in command line. I also got an Unit test that call the method MethodToTest :

[TestMethod]
public void TestMethodToTest()
{
    BomProviderMock mock = new BomProviderMock();
    BomManager bomManager = new BomManager(mock);

    List<int> list = new List<int>();
    for (int i = 0; i <= Int16.MaxValue; i++)
    {
        list.Add(i);
    }
    // Firts attempt with a non empty collection
    bomManager.MethodToTest(new Collection<int>(list));

    // Second attempt with an empty collection
    bomManager.MethodToTest(new Collection<int>());
}

After having used the tool opencover, the method MethodToTest got a 80% at branch coverage. My question is, does the foreach loop affect the branch coverage, if so, how can I get a 100% with this simple code?

oxman
  • 33
  • 1
  • 4
  • instead of writing a foreach could you not use the Collection's extension method `foreach` and create a delegate ? [List.foreach](http://msdn.microsoft.com/en-us/library/bwabdf9z(v=vs.110).aspx) – MethodMan Sep 30 '14 at 21:43
  • I run the code sample with an other code coverage tool (DotCover) and I got 100% code coverage. Could it be that `sum += value;` is not included because the code gets optimized and is not counted by your coverage tool? Adding a statement `Debug.WriteLine(sum);` could help in this case. – Sjips Sep 30 '14 at 21:49
  • OpenCover looks at the IL produced by compiling the code and sometimes the compiler adds more branch IL statements than you can map to your code hence it makes it difficult to get 100% branch coverage with OpenCover. Occasionally the team will look at optimizing some away but not often (I'll add this to the list of candidates). – Shaun Wilde Oct 01 '14 at 22:36

2 Answers2

5

As the [original] accepted answer has pointed out your actual scenario reduces to collection.Sum() however you will not be able to get away with this every time.

If we use TDD to develop this (overkill I agree but easy to explain) we would [possibly] do the following (I am also using NUnit in this example out of preference)

[Test]
public void Sum_Is_Zero_When_No_Entries()
{
    var bomManager = new BomManager();
    Assert.AreEqual(0, bomManager.MethodToTest(new Collection<int>()));
}

and then write the following code (note: we write the minimum to meet the current set of tests)

public int MethodToTest(Collection<int> collection)
{
    var sum = 0;
    return sum;
}

We would then write a new test e.g.

[Test]
[TestCase(new[] { 0 }, 0)]
public void Sum_Is_Calculated_Correctly_When_Entries_Supplied(int[] data, int expected)
{
    var bomManager = new BomManager();
    Assert.AreEqual(expected, bomManager.MethodToTest(new Collection<int>(data)));
}

If we ran our tests they would all pass (green) so we need a new test(cases)

[TestCase(new[] { 1 }, 1)]
[TestCase(new[] { 1, 2, 3 }, 6)]

In order to satisfy those tests I would need to modify my code e.g.

public int MethodToTest(Collection<int> collection)
{
    var sum = 0;
    foreach (var value in collection)
    {
        sum += value;
    }
    return sum;
}

Now all my tests work and if I run that through opencover I get 100% sequence and branch coverage - Hurrah!.... And I did so without using coverage as my control but writing the right tests to support my code.

BUT there is a 'possible' defect... what if I pass in null? Time for a new test to investigate

[Test]
public void Sum_Is_Zero_When_Null_Collection()
{
    var bomManager = new BomManager();
    Assert.AreEqual(0, bomManager.MethodToTest(null));
}

The test fails so we need to update our code e.g.

public int MethodToTest(Collection<int> collection)
{
    var sum = 0;
    if (collection != null)
    {
        foreach (var value in collection)
        {
            sum += value;
        }
    }
    return sum;
}

Now we have tests that support our code rather than tests that test our code i.e. our tests do not care about how we went about writing our code.

Now we have a good set of tests so we can now safely refactor our code e.g.

public int MethodToTest(IEnumerable<int> collection)
{
    return (collection ?? new int[0]).Sum();
}

And I did so without affecting any of the existing tests.

I hope this helps.

Mephy
  • 2,978
  • 3
  • 25
  • 31
Shaun Wilde
  • 8,228
  • 4
  • 36
  • 56
  • +1 for TTD and good example. don't you need your tests to return in NUnit, when you use the `TestCase` though ? – Noctis Oct 01 '14 at 23:51
  • Hmm ... here's an article I wrote on [Unit testing](http://www.codeproject.com/Articles/784791/Introduction-to-Unit-Testing-with-MS-tests-NUnit-a). If you have a `void` method with `TestCase(...)` header, I'm getting a: `Test ignored: Test method has a void return type, but a result is expected` (using nunit) – Noctis Oct 01 '14 at 23:59
  • @Noctis are you perhaps using Resharper? if so you need to upgrade as this is an old issue http://stackoverflow.com/questions/9241232/does-nunit-testcase-attribute-with-result-property-work-incorrect – Shaun Wilde Oct 02 '14 at 01:55
  • Yep, I am. The latest and greatest though (8.2.2). The link you posted is for the other way around (having a return on something that is supposed to be void). My comment is about it NEEDING a result, but your signature is void. – Noctis Oct 02 '14 at 03:00
  • Works fine for me on the command line and in VS with ReSharper - all 5 tests run (no warnings/messages) perhaps you need to expand more on what you think the issue is. – Shaun Wilde Oct 02 '14 at 07:08
  • Ah, I see, what you failed to mention is that you are using `Result = 0` etc as part of the test case (this is not actually required - or never used to be); for that to work you need to change the signature of the test to `public int Sum_Is_Calculated_Correctly_When_Entries_Supplied(int[] data)` and then return the result of `MethodToTest`. – Shaun Wilde Oct 02 '14 at 07:16
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/62318/discussion-between-noctis-and-shaun-wilde). – Noctis Oct 02 '14 at 08:03
2

I've Updated your question a bit, using some linq instead of the foreach loops. It takes a random number (same size list though), so the compiler won't "whisk it away" and have to compute it.

I would suggest doing something with the sum in the method, or returning it, it might change your results.

Last, but not least, don't obsess about 100%. It'll never happen in a real life big project. Just make sure you test the things that might brake, and build your software with testing in mind, so it'll be easy to do.

void Main()
{
    Random r = new Random();
    List<int> list = Enumerable.Range(1,Int16.MaxValue)
                               .Select (e => r.Next(0,Int16.MaxValue))
                               .ToList();

    // Firts attempt with a non empty collection
    MethodToTest(new Collection<int>(list));

    // Second attempt with an empty collection
    MethodToTest(new Collection<int>());
}

// Define other methods and classes here
public void MethodToTest(Collection<int> collection)
{
    var sum = collection.Sum (i => i);
    // do something with it. If you're just voiding it and it doesn't get 
    // used, it might be removed by compiler.
}
Noctis
  • 11,507
  • 3
  • 43
  • 82
  • I agree - Writing tests to get coverage is the wrong to go about testing and 100% branch coverage is usually not possible in real-life (also you will go mad if you obsess over it). Look to writing the right tests and/or using TDD practices and following SOLID principles, it looks hard work at first but pays benefits on the long run. – Shaun Wilde Oct 01 '14 at 22:37
  • you should have +1'ed it, to show your appreciation :) (having said that, it's always nice to see round numbers on your stats ...like ... 100%) – Noctis Oct 01 '14 at 23:30
  • I was not getting 100% branch coverage with a foreach loop but with a Collection.Sum() I was able... Finally, the problem was the version of OpenCover. I was using the v.4.5.2506 and updated to v.4.5.3207 to acheive 100% branch coverage with a foreach loop. Although, those responses are pretty useful. Thanks. – oxman Oct 07 '14 at 20:13