Modules/businessdev.ALbuild.Apps/Public/Get-BcTestQuality.ps1

function Get-BcTestQuality {
    <#
    .SYNOPSIS
        Heuristically assesses the QUALITY of AL tests (assertions, arrange/act/assert) -- not their coverage.
 
    .DESCRIPTION
        Code coverage is not test quality: a [Test] method can execute every line of the code under test and
        still assert nothing, so it would show as "covered" while catching no regression. This scans the AL [Test]
        methods in a workspace and flags quality smells per test:
 
          * NoAssertions - the body contains no assertion (no Assert/LibraryAssert call, no TestField, no
                            asserterror, no guard Error()), so it cannot fail on a wrong result.
          * Empty - the body has no executable statements.
          * NoAct - the body asserts but never invokes anything outside the assertion library (a test
                            that only checks constants).
 
        It returns a per-test breakdown and a suite summary with a 0-100 quality score (share of tests that
        actually assert, penalised for empty tests). The heuristics are intentionally conservative and documented;
        treat the score as a smell detector, not a grade.
 
    .PARAMETER WorkspaceRoot
        AL source root to scan for [Test] methods (skips tooling folders, like Get-BcAlObjectMap).
 
    .PARAMETER Path
        A single .al file to assess instead of a whole workspace.
 
    .OUTPUTS
        PSCustomObject: Summary, Tests[].
    #>

    [CmdletBinding(DefaultParameterSetName = 'Workspace')]
    [OutputType([PSCustomObject])]
    param(
        [Parameter(Mandatory, ParameterSetName = 'Workspace')] [ValidateNotNullOrEmpty()] [string] $WorkspaceRoot,
        [Parameter(Mandatory, ParameterSetName = 'File')] [ValidateNotNullOrEmpty()] [string] $Path
    )

    # Assertion signals: the Library Assert codeunit, TestField/FieldError, the asserterror keyword, and a guard
    # Error() (the hand-rolled assert pattern `if <unexpected> then Error(...)`).
    $assertRx = [regex]::new('(?i)(\b(assert|libraryassert|assertion)\s*\.)|(\.testfield\s*\()|(\.fielderror\s*\()|(\basserterror\b)|(\berror\s*\()')
    $callRx = [regex]::new('[A-Za-z0-9_"]\s*\(')   # a procedure/method call on the line

    $scanArgs = if ($PSCmdlet.ParameterSetName -eq 'File') { @{ Path = $Path } } else { @{ WorkspaceRoot = $WorkspaceRoot } }

    $tests = [System.Collections.Generic.List[object]]::new()
    foreach ($proc in (Get-BcAlTestProcedure @scanArgs)) {
        $stmtCount = @($proc.Statements).Count
        $assertCount = 0; $callCount = 0
        foreach ($code in $proc.Statements) {
            if ($assertRx.IsMatch($code)) { $assertCount++ }
            elseif ($callRx.IsMatch($code)) { $callCount++ }
        }

        $issues = [System.Collections.Generic.List[string]]::new()
        if ($stmtCount -eq 0) { $issues.Add('Empty') }
        if ($assertCount -eq 0 -and $stmtCount -gt 0) { $issues.Add('NoAssertions') }
        if ($assertCount -gt 0 -and $callCount -eq 0 -and $stmtCount -gt 0) { $issues.Add('NoAct') }

        $tests.Add([PSCustomObject]@{
                codeunit       = $proc.Codeunit
                testName       = $proc.TestName
                filePath       = $proc.FilePath
                line           = $proc.Line
                statementCount = $stmtCount
                assertionCount = $assertCount
                hasAssertions  = $assertCount -gt 0
                invokesCode    = $callCount -gt 0
                issues         = @($issues)
            })
    }

    $testCount = $tests.Count
    $withAsserts = @($tests | Where-Object { $_.hasAssertions }).Count
    $empty = @($tests | Where-Object { $_.issues -contains 'Empty' }).Count
    $noAssert = @($tests | Where-Object { $_.issues -contains 'NoAssertions' }).Count
    # Score: share asserting, with empty tests counted as worst-case (0).
    $score = if ($testCount -gt 0) { [math]::Round((($withAsserts) / $testCount) * 100, 2) } else { 100.0 }
    $summary = [PSCustomObject]@{
        testCount             = $testCount
        testCodeunitCount     = @($tests | Select-Object -ExpandProperty codeunit -Unique).Count
        testsWithAssertions   = $withAsserts
        testsWithoutAssertions = $noAssert
        emptyTests            = $empty
        assertionDensity      = if ($testCount -gt 0) { [math]::Round((($tests | Measure-Object assertionCount -Sum).Sum / $testCount), 2) } else { 0.0 }
        qualityScore          = $score
    }

    Write-ALbuildLog -Level Success ("Test quality: {0}/{1} test(s) assert ({2}% score); {3} without assertions, {4} empty." -f $withAsserts, $testCount, $score, $noAssert, $empty)
    return [PSCustomObject]@{ Summary = $summary; Tests = @($tests) }
}