Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Problem mocking class types for variable declarations #90

Closed
johlju opened this issue Jul 31, 2016 · 10 comments
Closed

Problem mocking class types for variable declarations #90

johlju opened this issue Jul 31, 2016 · 10 comments
Labels
discussion The issue is a discussion.

Comments

@johlju
Copy link
Member

johlju commented Jul 31, 2016

I need to figure out if this is possible to accomplish. Any comments appreciated. Trying to mock a DSC Resource on a system that does not have SQL Server. Doing that I came across this problem that I have a used specific types for variables. Like the example below, it uses the type Microsoft.SqlServer.Management.Smo.PermissionSetBase.

function Merge-SQLPermissionSet {
    param (
        [Parameter(Mandatory)]
        [Microsoft.SqlServer.Management.Smo.PermissionSetBase[]]
        [ValidateNotNullOrEmpty()]
        $Object
    )

    $baseObject = New-Object -TypeName ($Object[0].GetType())

    foreach ( $currentObject in $Object ) {
        foreach( $Property in $($currentObject | Get-Member -Type Property) ) {
            if( $currentObject.$($Property.Name) ) {
                $baseObject.$($Property.Name) = $currentObject.$($Property.Name)
            }
        }
    }

    return $baseObject
}

I tried to solve that by using a class and adding that as a type (below example is simplified).

$mockPermissionSetBase = @"
    namespace Microsoft.SqlServer.Management.Smo
    {
        public class PermissionSetBase 
        {
            public PermissionSetBaset(){}
            public bool MyValue = false;
        }
    }
"@

Add-Type -TypeDefinition $mockPermissionSetBase

But this does not work since that type will only exist during runtime, not during parsing, giving me syntax errors (missing types in variables declarations).
Adding it as a PowerShell class does not work either since it classes does not support namespaces.

The only way I see past this is to use [Object]. But would really like to keep the types as-is.

@mbreakey3 do you have any feedback on this?

@johlju
Copy link
Member Author

johlju commented Aug 1, 2016

Okay, so this was be being a noob :) I got this working. Brought out the old C# knowledge and got this working.

Sorry friends.

@johlju johlju closed this as completed Aug 1, 2016
@kilasuit
Copy link

kilasuit commented Aug 1, 2016

@johlju Check out the SharePointDSC resource as this is one where they do a lot of mocking within the DSC tests for on the Appvayor builds where SharePoint won't be installed either.

I would also recommend getting in touch with @BrianFarnhill for more background questions as I know that SharePoint has alot of similar reliance on the object types as SQL will do too though I know in SharePointDSC they don't mock the object the same way as you are looking to do so.

@BrianFarnhill
Copy link

I can definitely help with this - I wrote a blog post about how we did it for SharePointDsc - happy to get involved here to help implement something similar for xSqlServer as well (I'm working with a colleague on contributing to this resource at the moment anyway!)

@johlju
Copy link
Member Author

johlju commented Aug 2, 2016

Thanks for all the feedback! Really appreciate how we help each other out.

@BrianFarnhill i checked out your blog post and also glanced at the SharePointDsc resource, and the way you did stubs. Unfortunately in SQL Server we are lacking a lot of cmdlets. What we do have is the 'SMO model'. The .net classes with namespace Microsoft.SqlServer.Management.Smo.
So I needed a way to mock these classes to actually use it as types and methods. First I tried using PowerShell classes but that didn't work since namespaces don't work. I then tried to mock using PowerShell classes using fake class names, and changing the TypeName of the classes for them to look similar. This worked until I got to the point that I hit a function that was using the SMO types in the variable declaration. I tried C# classes in here strings, but at first I didn't get that working. At this point I change all those types to [object] instead, and I filled this issue for some feedback. But that didn't feel good doing that change just to make the test work.
After a good night sleep, and after long evening walk I managed to get it working ;)

I am using a stub file, just like SharePointDsc, but the stub file is a C# file SMO.cs to be able to get intellisense and lower the risk to get errors in the code. See the file in the PR here https://github.com/PowerShell/xSQLServer/pull/53/files#diff-8cfaa2961050d03cd15ee2f21cb0426f.
This stub file is then loaded into the test with Add-Type -Path (Join-Path -Path $script:moduleRoot -ChildPath 'Tests\Unit\Stubs\SMO.cs')

Appreciate more feedback on this. If there is any way to do this even better, lets make those changes.

@johlju
Copy link
Member Author

johlju commented Aug 2, 2016

Reopen this issue for while so we can discuss this.

@johlju
Copy link
Member Author

johlju commented Aug 2, 2016

Was thinking when I coded the mock classes that looping thru entire 'SMO model' and build mock classes and enums might be helpful. These could then be improved with necessary logic.
But that is, if using these mock classes is the right way to go.

@kwirkykat kwirkykat added the discussion The issue is a discussion. label Aug 2, 2016
@BrianFarnhill
Copy link

Yea so with SharePointDsc we've done a few things when we need to implement specific objects as opposed to mocking PowerShell cmdlets.

The first is that we do the Add-Type thing with some stubs for some of them where we needed an object that we don't really care what we do with it - like our search topology ones, we need an object with a specific type name/namespace but other than that its just properties on an object, so we mock those out like this:

Add-Type -TypeDefinition "public class AdminComponent { public string ServerName { get; set; } public System.Guid ComponentId {get; set;}}"

That sort of thing we could automate the generation of pretty easily - load the required assemblies then get objects and namespaces and iterate through those. I would be saving those out to a .cs file and then importing that in rather than coding them in directly like we do here.

The second thing we do where we need to return specific things from methods on those objects is spin them up in PowerShell like this:

            New-Object Object |            
                Add-Member NoteProperty TypeName "User Profile Service Application" -PassThru |
                Add-Member NoteProperty DisplayName $testParams.Name -PassThru | 
                Add-Member NoteProperty ApplicationPool @{ Name = $testParams.ApplicationPool } -PassThru |             
                Add-Member ScriptMethod GetType {
                    New-Object Object |
                        Add-Member ScriptMethod GetProperties {
                            param($x)
                            return @(
                                (New-Object Object |
                                    Add-Member NoteProperty Name "SocialDatabase" -PassThru |
                                    Add-Member ScriptMethod GetValue {
                                        param($x)
                                        return @{
                                            Name = "SP_SocialDB"
                                            Server = @{ Name = "SQL.domain.local" }
                                        }
                                    } -PassThru
                                ),
                                (New-Object Object |
                                    Add-Member NoteProperty Name "ProfileDatabase" -PassThru |
                                    Add-Member ScriptMethod GetValue {
                                        return @{
                                            Name = "SP_ProfileDB"
                                            Server = @{ Name = "SQL.domain.local" }
                                        }
                                    } -PassThru
                                ),
                                (New-Object Object |
                                    Add-Member NoteProperty Name "SynchronizationDatabase" -PassThru |
                                    Add-Member ScriptMethod GetValue {
                                        return @{
                                            Name = "SP_ProfileSyncDB"
                                            Server = @{ Name = "SQL.domain.local" }
                                        }
                                    } -PassThru
                                )
                            )
                        } -PassThru
            } -PassThru -Force 

Now the benefit here is that we can control in PowerShell what gets returned from specific methods, which is a plus. We can also do things like set global variables inside the ScriptMethods so we can poll those to check that the methods were called (because we can't do an Assert-MockCalled on those). Now we could come up with some approaches like this that would work as well assuming you don't rely on the GetType() method in your code at all (because the type will be different), but it would be more complex to generate these. Currently we write these up by hand in our mocks (because we also want them to return different things under different contexts) and I would recommend that the same approach be taken here for the places that you need them.

Things like enums are a no brainer - we can script those up to a cs file and import those, but I think we need to do some careful thinking about how you want to implement the other objects that you will actually interact with in your tests to make sure you set yourself up for better success.

@johlju
Copy link
Member Author

johlju commented Aug 3, 2016

Okay so you just use [Object] instead of the actual class names. Do you ever use any classes with their namespace as types?
Like below where Microsoft.SqlServer.Management.Smo.ServerPermissionSet is used in the first function as both parameter type and getting properties of the type, and in the second function the type is used both in the parameter declaration and as a local variable.

Full resource code here

function Get-SQLPermission
{
    [CmdletBinding()]
    [OutputType([String[]])]
    param (
        [Parameter(Mandatory,ParameterSetName="ServerPermissionSet",HelpMessage="Takes a PermissionSet which will be enumerated to return a string array.")]
        [Microsoft.SqlServer.Management.Smo.ServerPermissionSet]
        [ValidateNotNullOrEmpty()]
        $ServerPermissionSet
    )

    [String[]] $permission = @()

    if( $ServerPermissionSet ) {
        foreach( $Property in $($ServerPermissionSet | Get-Member -Type Property) ) {
            if( $ServerPermissionSet.$($Property.Name) ) {
                $permission += $Property.Name
            }
        }
    }

    return [String[]] $permission
}

function Get-SQLServerPermissionSet
{
    [CmdletBinding()]
    [OutputType([Object])] 
    param
    (
        [Parameter(Mandatory,ParameterSetName="Permission",HelpMessage="Takes an array of strings which will be concatenated to a single ServerPermissionSet.")]
        [System.String[]]
        [ValidateNotNullOrEmpty()]
        $Permission,

        [Parameter(Mandatory,ParameterSetName="ServerPermissionSet",HelpMessage="Takes an array of ServerPermissionSet which will be concatenated to a single ServerPermissionSet.")]
        [Microsoft.SqlServer.Management.Smo.ServerPermissionSet[]]
        [ValidateNotNullOrEmpty()]
        $PermissionSet
    )

    if( $Permission ) {
        [Microsoft.SqlServer.Management.Smo.ServerPermissionSet] $permissionSet = New-Object -TypeName Microsoft.SqlServer.Management.Smo.ServerPermissionSet

        foreach( $currentPermission in $Permission ) {
            $permissionSet.$($currentPermission) = $true
        }
    } else {
        $permissionSet = Merge-SQLPermissionSet -Object $PermissionSet 
    }

    return $permissionSet
}

How would you go about this? Mock the type using New-Object Object ... like you did above, and replace these types from the code with [object]? Or is there a better solution?
Or should we focus not to use types like this?
You are somewhat into the notion of a .cs file. Is that the right way if we need to use classes with namespaces as types like here to create the as C# in a .cs file? Like I did see code here.

To be able to 'mock the right state' I had to make a constructor that sets a variable to remember the state, see _generateMockData in the class Server. I imagine this will become a problem depending of what needs to be mocked. You can't add parameters to a method. When I wrote the C# code I thought that eventually it might be necessary with some "global variable" in the class that knows if it should mock data or not.

// smo.cs
namespace Microsoft.SqlServer.Management.Smo
{
    public class Globals
    {
        public static bool GenerateMockData = false;
    }
....
    public class Server 
    { 
        ....
            if( Globals.GenerateMockData ) {
                Microsoft.SqlServer.Management.Smo.ServerPermissionSet[] permissionSet = { 
                    new Microsoft.SqlServer.Management.Smo.ServerPermissionSet( true, false, false, false ),
                    new Microsoft.SqlServer.Management.Smo.ServerPermissionSet( false, true, false, false ),
                    new Microsoft.SqlServer.Management.Smo.ServerPermissionSet( false, false, true, false ),
                    new Microsoft.SqlServer.Management.Smo.ServerPermissionSet( false, false, false, true ) };

                listOfServerPermissionInfo.Add( new Microsoft.SqlServer.Management.Smo.ServerPermissionInfo( permissionSet ) );
            } else {
                listOfServerPermissionInfo.Add( new Microsoft.SqlServer.Management.Smo.ServerPermissionInfo() );
            }
        ....
    }
}

# Tests.ps1
Mock -CommandName Get-SQLPSInstance -MockWith { 
    [Microsoft.SqlServer.Management.Smo.Globals]::GenerateMockData = $true

    $mockObjectSmoServer = New-Object Microsoft.SqlServer.Management.Smo.Server
    $mockObjectSmoServer.Name = "$nodeName\$instanceName"
    $mockObjectSmoServer.DisplayName = $instanceName
    $mockObjectSmoServer.InstanceName = $instanceName
    $mockObjectSmoServer.IsHadrEnabled = $False
    $mockObjectSmoServer.MockGranteeName = $principal

    return $mockObjectSmoServer
} -ModuleName $script:DSCResourceName -Verifiable

Ps. I would have loved to use PowerShell classes if they supported namespaces. That would have been awesome to get C# like coding and PowerShell efficiency at the same time. Ds.

@johlju
Copy link
Member Author

johlju commented Aug 12, 2016

@BrianFarnhill I generated stub files for SQLPS and SQLServer modules using the code on your blog. I change it a bit to get the formatting as close as possible. The stub files are included in the PR #98.
Question: Where do you keep this "helper" function to generate a new stub file in the SharePoint resource? I expect the new SQL Server module to change frequently so it will be needed to run this now and again. So I think I need to put this in a good place for anyone to run.

Modified "helper" function to generate stub module file:

function Write-ModuleStubFile {
    param
    (
        [Parameter( Mandatory )] 
        [System.String] $ModuleName,

        [Parameter( Mandatory )] 
        [System.String] $StubPath
    )

    Import-Module $ModuleName -DisableNameChecking -Force

    ( ( get-command -Module $ModuleName -CommandType 'Cmdlet' ) | ForEach-Object -Begin { 
        "# Suppressing this rule because these functions are from an external module"
        "# and are only being used as stubs",
        "[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingUserNameAndPassWordParams', '')]"
        "param()"
        ""
    } -Process {
        $signature = $null
        $command = $_
        $endOfDefinition = $false
        $metadata = New-Object -TypeName System.Management.Automation.CommandMetaData -ArgumentList $command
        $definition = [System.Management.Automation.ProxyCommand]::Create($metadata) 
        foreach ($line in $definition -split "`n")
        {
            $line = $line -replace '\[Microsoft.SqlServer.*.\]', '[object]'
            $line = $line -replace 'SupportsShouldProcess=\$true, ', ''

            if( $line.Contains( '})' ) )
            {
                $line = $line.Remove( $line.Length - 2 )
                $endOfDefinition = $true
            }

            if( $line.Trim() -ne '' ) {
                $signature += "    $line"
            } else {
                $signature += $line
            }

            if( $endOfDefinition )
            {
                $signature += "`n   )"
                break
            }
        }

        "function $($command.Name) {"
        "$signature"
        ""
        "   throw '{0}: StubNotImplemented' -f $`MyInvocation.MyCommand"
        "}"
        ""
    } ) | Out-String | Out-File $StubPath -Encoding utf8 -Append
}

@johlju
Copy link
Member Author

johlju commented Jan 12, 2017

Closing this. I think it is obsolete now. We are long past this now. :) Thanks all!

@johlju johlju closed this as completed Jan 12, 2017
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
discussion The issue is a discussion.
Projects
None yet
Development

No branches or pull requests

4 participants