Lost in scopes

testmodule.psm1

function Invoke-Public ($param)
{
    Invoke-Private -param $param
}
function Invoke-Private ($param)
{
    try
    {
        $Res = Resolve-DnsName -Name $param -DnsOnly -ErrorAction Stop
        return $Res
    }
    catch
    {
        return $_
    }
}
Export-ModuleMember -Function Invoke-Public

testmodule.Tests.ps1

Import-Module testmodule
InModuleScope testmodule {
    $FQDN = "fqdn.domain.com"
    $Netbios = "hostname"
    Mock Resolve-DnsName {
        if ($Name -eq $FQDN)
        {
            return "true Name $Name. FQDN - $FQDN"
        }
        elseif ($Name -eq $Netbios)
        {
            return "false Name $Name. Netbios - $Netbios"
        }
        else
        {
            return "3rd Name $Name. FQDN - $FQDN. Netbios - $Netbios"
        }
    }
    Describe 'Private function test' {
        It 'Passes Invoke-Private with ' -TestCases @(
            @{Value = $FQDN; Expected = "true Name $FQDN. FQDN - $FQDN"}
            @{Value = $Netbios; Expected = "false Name $Netbios. Netbios - $Netbios"}
        ) {
            param ($Value, $Expected)
            Invoke-Private -param $Value | Should -Be $Expected
        }
    }
}
Describe 'Public function test' {
    $FQDN = "fqdn.domain.com"
    It 'Passes Invoke-Public' {
        Invoke-Public -param $FQDN | Should -Be "true Name $FQDN. FQDN - $FQDN"
    }
}

tests result:

Executing Test Executing all tests in '..\test'

Executing script C:\Temp\test\testmodule.Tests.ps1

Describing Private function test
[+] Passes Invoke-Private with ‘fqdn.domain.com’ 4.04s
[+] Passes Invoke-Private with ‘hostname’ 284ms

Describing Public function test
[-] Passes Invoke-Public 1.21s
Expected strings to be the same, but they were different.
Expected length: 49
Actual length: 45
Strings differ at index 0.
Expected: ‘true Name fqdn.domain.com. FQDN - fqdn.domain.com
But was: '3rd Name fqdn.domain.com. FQDN - . Netbios - '
-----------^
32: Invoke-Public -param $FQDN | Should -Be “true Name $FQDN. FQDN - $FQDN”
at , C:\Temp\test\testmodule.Tests.ps1: line 32
Tests completed in 5.54s
Tests Passed: 2, Failed: 1, Skipped: 0, Pending: 0, Inconclusive: 0

Why the $FQDN and $Netbios variables are not available for mocked Resolve-DnsName function? How should I make them available, or what would be the recommended way to test in this scenario?

(Apologies for the spurious incorrect forum post you may have received; that was a bot that didn’t catch the right keywords)

Gimme a sec to try this on my own!

Yeah, so the problem is just that you’ve passed out of scope for the mock. Mocks are really designed to mock a function that is being run in a test; they’re not designed to mock functions that exist elsewhere in a module. The module’s “local” definition of the function or command “wins,” because it’s more local. Because you’re calling a function, which is calling a function, the mock gets “lost,” if you will.

Struggling to explain, but maybe think of it this way: you’ve seen that you can go “one level” deep with Invoke-Private. What you can’t do is go “deeper.” Does that make sense?

I am not sure I understand, to me it seems that the mock is working, it returns the mocked string just with empty variables. My observation is that the variables defined InModuleScope are available when calling mocked function in that same scope, but they are not available when calling in public scope (from within a public function).

Now I’m on my phone and can’t double check, but I think I was getting the expected result if I just used strings in if checks in mocked function, instead of using $fqdn and $netbios variables.

Your observation is correct. The public function will not have access to those variables because it it outside the scope of where the mock is defined. If you move the public function test into that scope(InModuleScope testmodule) then the mocked outputs will be available to your public function.

I am getting a different result than you do. The last Describe is placed outside of the InModuleScope, correct?

Because in that case the Mock should not apply to the last describe, and that is what I am seeing. The real Resolve-DnsName is called.

— In general about the scopes, mock is designed to mock stuff in different scopes, not just the one in the test. So what you are doing is correct. You are reaching into the TestModule scope, and shadowing Resolve-DnsName there. That means that any call to Resolve-DnsName within that scope will call the mock an not the real function. Even if you call Invoke-Public, it will reach into the TestModule module scope, and invoke Invoke-Private there. Inside it invokes Resolve-DnsName which is the mock.

What would not work, and what people sometimes find confusing, is that we cannot overwrite the function directly - that is inject mock to the DnsClient module from which the Resolve-DnsName originates, because your module already got link to the functions from that module, and shadowing the functions in that module does not overwrite the link. So you are correctly mocking it in the scope of your mock, not in the scope of DnsClient.