Search code examples
powershellclassgithub-actionspesterpowershell-module

PowerShell module with class defined in separate file fails Pester tests in GitHub Actions


I am creating a PowerShell module that defines a class. For example:

class MyClass
{
    [string] $Name
}

If I put the class definition directly in the psm1 file then everything works fine. However, if I move the class to its own file and include it using using module .\PathTo\MyClass.ps1 then Pester fails in GitHub Actions with the error message:

Unable to find type [MyClass]

I have created this small sample repo that reproduces the problem.

The weird thing is that if I start a new PowerShell session on my local machine and run Invoke-Pester it all works as expected; it seems to only fail in GitHub Actions.

Below are the 2 modules' files and their Pester tests.

First, the module that defines the class directly in the psm1 file and everything works fine everywhere:

ModuleWithClassInPsm1.psm1:

class MyClassInPsm1
{
    [string] $Name
}

function Get-MyClassInPsm1
{
    return [MyClassInPsm1]::new()
}

Export-ModuleMember -Function Get-MyClassInPsm1

ModuleWithClassInPsm1.Tests.ps1:

using module .\ModuleWithClassInPsm1.psm1

Describe 'Module' {
    It 'Should return a new class instance without error' {
        $instance = Get-MyClassInPsm1
        $instance | Should -Not -Be $null
    }
}

And here is the other module that defines the class in a separate file:

Classes\MyClassInSeparateFile.ps1:

class MyClassInSeparateFile
{
    [string] $Name
}

ModuleWithClassInSeparateFile.psm1:

using module .\Classes\MyClassInSeparateFile.ps1

function Get-MyClassInSeparateFile
{
    return [MyClassInSeparateFile]::new()
}

Export-ModuleMember -Function Get-MyClassInSeparateFile

ModuleWithClassInSeparateFile.Tests.ps1:

using module .\ModuleWithClassInSeparateFile.psm1

Describe 'Module' {
    It 'Should return a new class instance without error' {
        $instance = Get-MyClassInSeparateFile
        $instance | Should -Not -Be $null
    }
}

I have verified that both my local machine and GitHub Actions are using Pester v5.5.0. I am using PowerShell v7.3.6 on my local Windows machine, while GitHub Actions is using PowerShell v7.2.13 on both Ubuntu and Windows. You can view the failing GitHub Actions here.

The PowerShell class docs and using docs state:

The using module statement imports classes from the root module (ModuleToProcess) of a script module or binary module. It doesn't 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.

I guess in that statement "module" means just the psm1 file itself, and not the other things/files brought into the module? It is strange thought that everything works fine locally (in a fresh PowerShell session), but not in the GitHub Actions. Maybe it's just a bug with GitHub Actions?

I plan on defining several classes in my module, and am following the convention of putting each function in its own file within a Public or Private directory (shown here and here, so I'd like to also put each class in its own file instead of stuffing them all in the psm1 file.

I'm open to any suggestions. Thanks in advance!


Solution

  • MadBoy's answer of using dot-sourcing and Import-Module will work in some scenarios, but it does not give the full picture. Using the sample repo I created, I setup various tests and found the following.

    Using PowerShell classes

    The only way to be able to reference the class type by name outside of the module (e.g. [MyClass]::new()) is:

    1. The class must be defined directly in the psm1 file; it cannot be dot-sourced in.
    2. The module must be imported with using module.

    If you use dot-sourcing or Import-Module, everything within the module itself can reference the class name fine, but if something outside of the module references the class name it will get the Unable to find type error.

    Using C# classes

    Everything just works when using C# classes. You can define them in separate files and dot-source them into the .psm1 file, and they will be fully available within the module and to consumers of the module whether they use Import-Module or using module.

    This is an example of what MyClass would look like as a C# class in PowerShell:

    Add-Type -Language CSharp -TypeDefinition @"
      public class MyClass {
          public string Name { get; set; }
      }
    "@
    

    For more details, check out this blog post.