gomutation-testing

Why does Golang's coverage report mark case statements in a switch as not tracked?


I'm running into an issue with Golang's coverage report where case statements within a switch block are marked as not tracked, even though I have written tests that execute these cases. Is this a known issue with Go's coverage tool, or am I missing something in my tests? How can I ensure that each case statement is accurately tracked in the coverage report?

Why does this matter?

I am running mutation testing on my code (https://gremlins.dev/latest/). This tool mutates the lines of code that are covered by the existing unit tests. Now, the conditions in the case statement can easily be mutated e.g. len(mobile) < 10 can be mutated to len(mobile) > 10. But since the line is not tracked, it is not considered for mutation. And hence, the concern.

Code:

func SanitizeMobile(mobile string) (string, serror.SError) { // Not tracked
    // Remove all non-digit characters // Covered
    var cleanedMobile strings.Builder // Covered

    for _, char := range mobile { // Covered
        if unicode.IsDigit(char) { // Covered
            cleanedMobile.WriteRune(char) // Covered
        } // Covered
    } // Covered

    mobile = cleanedMobile.String() // Covered

    switch { // Covered
    case len(mobile) < 10 || len(mobile) > 12: // Not tracked
        // After cleaning up all non-digits, mobile number cannot be greater than 12 digits in length // Covered
        return "", serror.New(serror.BadRequestError, InvalidMobile, nil) // Covered

    case len(mobile) == 10 && mobile[0] == '0': // Not tracked
        // In case mobile number is 10 digits long, we don't allow it to start with 0 // Covered
        // As of now, first digit of the mobile number can only be 5,6,7,8,9 but we relax // Covered
        // this criteria a bit, to allow 1,2,3,4 as well. // Covered
        return "", serror.New(serror.BadRequestError, InvalidMobile, nil) // Covered

    case len(mobile) == 11 && mobile[0] != '0': // Not tracked
        return "", serror.New(serror.BadRequestError, InvalidMobile, nil) // Covered

    case len(mobile) == 12 && mobile[0:2] != "91": // Not tracked
        return "", serror.New(serror.BadRequestError, InvalidMobile, nil) // Covered

    default: // Not tracked
        break // Covered
    } // Not tracked

    return mobile[len(mobile)-constant.IndianMobileLen:], nil // Covered
}

Coverage report: enter image description here

Coverage report when converted to if-else chain: enter image description here


Solution

  • // Not tracked indicates that coverage is not considered for that line. i.e. covered or not covered makes no difference

    However, the fact that the associated case block is itself covered makes this irrelevant.

    Why it doesn't matter

    If a case block is not executed, then this will be reflected in the coverage report as uncovered code regardless of whether it was not executed due to the associated case condition evaluating false or not being evaluated at all.

    To cover any uncovered case block, a test suite must ensure that the associated case condition evaluates true in at least one test case. In order to achieve that, the case condition must be reachable, dealing with both possible reasons for an uncovered case block.

    vs. else if

    I would be curious to see an example of similar observations involving else if statements.

    In a quick test (using Golang 1.22.2), I could not create a scenario where the condition in an else if was not tracked. However, the else clause in the statement (specifically) does appear to be not tracked:

    enter image description here