Pester icon indicating copy to clipboard operation
Pester copied to clipboard

Creating a mocked class by inheriting does not work when using dot source of class files

Open tripleacoder opened this issue 3 years ago • 6 comments

Checklist

What is the issue?

I want to create a mocked class by inheriting an existing class B. The purpose is to mock one of its methods.

However, when I do this, I get Unable to find type [B].

I am using a standard way of structuring my module where class files exist in a classes folder and are dot sourced in the .psm1 file.

Example of this structure: https://github.com/saidbrandon/PureStorage.FlashArray.Reporting/tree/master/PureStorage.FlashArray.Reporting

SO: https://stackoverflow.com/questions/74275024/unable-to-find-type-when-mocking-class-in-a-powershell-unit-test

Expected Behavior

Pester should not give the error "Unable to find type [B]"

Steps To Reproduce

Unit test file:


using module ".\MyModule.psd1" 

BeforeAll {
    Import-Module $PSScriptRoot\MyModule.psd1 -Force
}

Describe 'My unit test 1' {
    It 'Runs unit test 1' {
        
            # Mock a private function
            # Gives: Unable to find type [B].
            class MockedClassB : B
            {
                MockedClassB ([int]$foo) : base($foo)
                {
                }

                [string] MyMethod() { return 'MockedString' }
            }

            $mockedB = [MockedClassB]::new(13)
            $mockedB.Run()   
    }
}


Describe 'My unit test 2' {
    It 'Runs unit test 2' {
        
        # No errors.
        $instanceB = [B]::new(13)
        $instanceB.Run()   
    }
}

MyModule.psd1:

@{
RootModule = 'Include.psm1'
ModuleVersion = '1.0.0'

# This is needed to use classes. Otherwise the Test file will give "Unable to find type"
ScriptsToProcess = @(
      'Classes\B.ps1'
   )


RequiredModules = ''

# Functions to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no functions to export.
FunctionsToExport = @() 

# Cmdlets to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no cmdlets to export.
CmdletsToExport = @()

# Variables to export from this module
# VariablesToExport = '*'

# Aliases to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no aliases to export.
AliasesToExport = @()

}

Include.psm1:

#Requires -Version 5.0
[cmdletbinding()]
param()

Write-Verbose $PSScriptRoot
Write-Verbose 'Import Classes in order because of dependencies'
$classList = @(
    'B'
)

foreach($class in $classList)
{
    Write-Verbose " Class: $class"
    . "$psscriptroot\Classes\$class.ps1"
}

B.ps1:


using module ".\..\..\ModuleContainingClassA\ModuleContainingClassA.psd1"

class B: A
{
    # ctor
    B([int]$foo)
    {
    }
}

Describe your environment

Pester version : 5.3.3 PowerShell version : 5.1.19041.1682 OS version : Microsoft Windows NT 10.0.19044.0

Possible Solution?

The workaround for now is to remove the dot sourcing in the psm1 file and instead have all the classes inside the psm1 file itself.

tripleacoder avatar Nov 25 '22 12:11 tripleacoder

The workaround for now is to remove the dot sourcing in the psm1 file and instead have all the classes inside the psm1 file itself.

This isn't the same scenario, is it? Your sample above illustrates A and B belonging to different modules. Having both definitions in the same psm1 will only result in one module which removes a lot of complexity (module visibility, internal vs exported classes etc).

With two modules, simply calling using module .\MyModule.psd1 in the console also fails, so this isn't really Pester-specific and belongs more in the PowerShell-repo or in PowerShell-help at Discord. Classes are a PITA to debug in PowerShell, but I'm sure people with more experience can help you in the communities mentioned above. 🙂

As a workaround I'd suggest calling using module ".\..\..\ModuleContainingClassA\ModuleContainingClassA.psd1" prior to invoking Pester or importing MyModule. That way A will already be available in the session which I'd expect would work.

fflaten avatar Nov 27 '22 12:11 fflaten

The workaround for now is to remove the dot sourcing in the psm1 file and instead have all the classes inside the psm1 file itself.

This isn't the same scenario, is it? Your sample above illustrates A and B belonging to different modules. Having both definitions in the same psm1 will only result in one module which removes a lot of complexity (module visibility, internal vs exported classes etc).

With two modules, simply calling using module .\MyModule.psd1 in the console also fails, so this isn't really Pester-specific and belongs more in the PowerShell-repo or in PowerShell-help at [Discord]([https://aka.ms/psdiscord]](https://aka.ms/psdiscord%5D). Classes are a PITA to debug in PowerShell, but I'm sure people with more experience can help you in the communities mentioned above. 🙂

As a workaround I'd suggest calling using module ".\..\..\ModuleContainingClassA\ModuleContainingClassA.psd1" prior to invoking Pester or importing MyModule. That way A will already be available in the session and I'd expect it would work.

Thanks for taking a look at my issue.

I can repro the issue without the complication of an extra module.

I took this random Powershell module "HostsFileManagement" from Github: https://github.com/Stephanevg/HostsFileManagement

In the first test in HostFileManagement.Tests.ps1 I added a mocked class:

Import-Module -Force $PSScriptRoot\..\HostsFileManagement.psd1

InModuleScope HostsFileManagement {

  Describe 'HostsEntry - Constructors' {
  
    Context 'Testing Constructors: values with comment'{
      $IpAddress = "192.168.2.2"
      $HostName = "Woop"
      $fqdn = "woop.powershelldistrict.com"
      $Description = "Awesome Server"
      $Entry = [HostsEntry]::new($IpAddress,$HostName,$fqdn,$Description,[HostsEntryType]::Comment)
      It 'Ipaddress should be set' {

        class MockedClass : HostsEntry 
        {
        }

        $Entry.Ipaddress | should -be $IpAddress
      }

This gave "Unable to find type [HostsEntry]", but only on the line that inherits form the class, not on the previous line:

$Entry = [HostsEntry]::new($IpAddress,$HostName,$fqdn,$Description,[HostsEntryType]::Comment)

I then added a using line as you suggested, first with a relative path:

using module ".\HostsFileManagement.psd1"

Then I tried with an absolute path: using module "C:\Users\lpedersen\source\repos\IT_Operations\StructureTest\HostsFileManagement\HostsFileManagement.psd1"

But it made no difference.

tripleacoder avatar Nov 28 '22 09:11 tripleacoder

Maybe this will help. This is a workaround for the issue that PowerShell parser must have using module at the top of a script. Creating a scriptblock from a string. Below is running in Pester 5.

https://github.com/dsccommunity/SqlServerDsc/blob/8dde54df19ccbdb95629ec1c074e7a97acf229d2/tests/Unit/Classes/ResourceBase.Tests.ps1#L111-L165

Just for information: The only downside to this is that it somehow broke the new faster code coverage (that does not use breakpoints) that we used in our pipeline, so I had to switch over to use breakpoints. I have yet to create a repro for that issue, haven't had the time yet.

johlju avatar Nov 28 '22 16:11 johlju

That workaround is useful. I will update StackOverflow.

tripleacoder avatar Nov 29 '22 11:11 tripleacoder

Using the workaround I got rid of the issue in MyModule. I can now use dot-sourced classes in that module.

MyModule depends on a different module 'ModuleContainingClassA', and I have now dot-sourced the classes there also.

But now the Pester discovery error "Unable to find type [A]" comes back. The exception is thrown from B.ps1.

How do I fix that?

tripleacoder avatar Nov 29 '22 15:11 tripleacoder

I found this in the docs about using: https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_using?view=powershell-7.2#module-syntax

"The using module statement imports classes from the root module (ModuleToProcess) of a script module or binary module. It does not consistently import classes defined in nested modules or classes defined in scripts that are dot-sourced into the module. Classes that you want to be available to users outside of the module should be defined in the root module."

If I understand it correctly, I should not expect class A defined in ModuleContainingClassA to be available to other modules if I use dot-sourcing.

I also found this project: https://github.com/PoshCode/ModuleBuilder It seems to fix some of these issues by compiling the module files into a single psm1 file.

tripleacoder avatar Nov 30 '22 09:11 tripleacoder