New Types and Methods should be tested

Rule ND1601 Code Coverage

Runs only in diff mode (baseline defined) and when coverage data is imported, on types and methods added since baseline.

Covered code is maintainable and non-error-prone. Since stopping development to test a poorly covered code base is impracticable, each time a method or type gets added, write associated unit-tests. Maintainability increases, refactoring becomes driven by testability, and the overall design improves.

Severity is High: these issues reflect an actual trend of not writing tests for new code.

How to fix

Write unit-tests covering most methods and types added since the baseline.

CQLinq source

This rule is written in CQLinq, C# LINQ over the NDepend code model. It ships with every new NDepend project: you can read it, adapt it, or use it as a starting point for your own custom rules.

// <Name>New Types and Methods should be tested</Name>
// <Id>ND1601:NewMethodsShouldBeTested</Id>

warnif count > 0

// Gather typesMoved, types whose parent namespaces changed since baseline
let typesRemoved = context.CompareContext.OlderCodeBase.Application.Types
   .Where(t => t.WasRemoved()).Select(t => t.Name).ToHashSetEx()
let typesMoved = Application.Types
   .Where(t => t.WasAdded() && typesRemoved.Contains(t.Name)).ToHashSetEx()

// Gather methodsMoved keys: a method removed from a type of the baseline and added, with the
// same name (signature embedded), lines of code and IL size, to another type is relocated
// code (a class split into helpers): its coverage debt pre-dates the baseline, it is not new.
let methodsRemovedKeys = context.CompareContext.OlderCodeBase.Application.Methods
   .Where(m => m.WasRemoved() && m.NbILInstructions >= 10)
   .Select(m => m.Name + "|" + m.NbLinesOfCode.ToString() + "|" + m.NbILInstructions.ToString()).ToHashSetEx()

let newMethodsUntested = (from m in JustMyCode.Methods where
  m.NbLinesOfCode > 0 &&
  m.PercentageCoverage < 30 &&
  m.WasAdded() &&

   // Don't match methods considered as new because its parameters changed
  (!m.ParentType.IsPresentInBothBuilds() ||
   !m.ParentType.OlderVersion().MethodsAndConstructors.Any(m1 => m1.WasRemoved() && m1.SimpleNameEquals(m.SimpleName))) &&

  // Don't match methods in types moved, nor methods relocated to another type
  !typesMoved.Contains(m.ParentType) &&
  !methodsRemovedKeys.Contains(m.Name + "|" + m.NbLinesOfCode.ToString() + "|" + m.NbILInstructions.ToString())

select new {
   elem = (ICodeContainer)m, 
   // Simplistic Debt estimation, because the effort to write tests for a method not 100% tested
   // is already estimated properly with the rule "Code should be tested".
   debt = m.NbLinesOfCodeNotCovered.Linear(1,2, 10,10).ToMinutes().ToDebt(),
}).ToArray() // enumerate once

// newMethodsUntestedInNewTypeTested is all new methods not 100% covered with parent type more than 0% covered
let newMethodsUntestedInNewTypeTested= from x in newMethodsUntested where 
   x.elem.AsMethod.ParentType.PercentageCoverage > 0 ||
  !x.elem.AsMethod.ParentType.WasAdded()
   select x

// newTypesUntested contains all type 0% covered whose methods are in methodsUntested
let newTypesUntested = newMethodsUntested
   .Where(x => x.elem.AsMethod.ParentType.PercentageCoverage == 0 && 
               x.elem.AsMethod.ParentType.WasAdded())
   .ToLookup(x => x.elem.AsMethod.ParentType)
   .Select(@group => 
      new { 
         elem = (ICodeContainer)@group.Key, 
         debt = @group.Sum(m => m.debt) })

//from x in newMethodsUntested // use this clause to list all methods instead of types & methods
from x in newMethodsUntestedInNewTypeTested.Concat(newTypesUntested)

  orderby x.elem.NbLinesOfCode descending, 
          x.elem.NbLinesOfCodeNotCovered ,
          x.elem.PercentageCoverage

select new { 
  x.elem, 
  x.elem.PercentageCoverage,
  x.elem.NbLinesOfCode,
  x.elem.NbLinesOfCodeNotCovered,
  CyclomaticComplexity = x.elem.IsMethod ? x.elem.AsMethod.CyclomaticComplexity : x.elem.AsType.CyclomaticComplexity,
  Debt = x.debt, 
  Severity = Severity.High
}


//<Expl>The new *{0}* is {1}% covered by tests.
// It is important to direct the testing endeavor primarily towards newly developed code.</Expl>

//<Desc>
// Runs only in *diff mode* (baseline defined) and when coverage data is
// imported, on types and methods added since baseline.
//
// Covered code is *maintainable* and *non-error-prone*. Since stopping
// development to test a poorly covered code base is impracticable, each time
// a method or type gets added, write associated unit-tests. Maintainability
// increases, refactoring becomes driven by testability, and the overall
// design improves.
//
// Severity is **High**: these issues reflect an actual trend of not writing
// tests for new code.
//</Desc>

//<HowToFix>
// Write unit-tests covering most methods and types added since the baseline.
//</HowToFix>

Code Coverage: all 15 items