Separating Environmental Code from Structural Code in PowerShell Operational Validation Tests

Do you ever feel like you're writing the same operational validation or readiness test over and over again? I'm not sure about you, but I don't like repeating myself by rewriting the same code because it creates a lot of technical debt. There has to be a better way . Why not take the same thought process from DSC (Desired State Configuration) and separate the environmental portion of the code from the structural portion and apply it to operational tests so the same or similar code isn't repeated over and over again?

An article that I wrote for PowerShell Magazine on Eliminating Redundant Code by Writing Reusable DSC Configurations provides a good overview of the thought process that I'm going to apply to operational tests.

The following example shows a function that is used to perform validation of multiple environments for a specific application on one or more servers:

  1#Requires -Version 3.0 -Modules Pester, MrToolkit
  2function Test-MrAppServer {
  3    [CmdletBinding()]
  4    param (
  5        [Parameter(Mandatory,
  6                   ValueFromPipeline)]
  7        [string[]]$ComputerName,
  8
  9        [System.Management.Automation.Credential()]$Credential = [System.Management.Automation.PSCredential]::Empty
 10    )
 11
 12    PROCESS {
 13        foreach ($Computer in $ComputerName) {
 14
 15            Describe "Validation of application server: $Computer" {
 16                try {
 17                    $Session = New-PSSession -ComputerName $Computer -Credential $Credential -ErrorAction Stop
 18                }
 19                catch {
 20                    Write-Warning -Message "Unable to connect. Aborting Pester tests for computer: '$Computer'."
 21                    Continue
 22                }
 23
 24                Context 'Validating the Training environment' {
 25                    It "The 'Training Runtime Server' service should be running" {
 26                        (Invoke-Command -Session $Session {Get-Service -Name '*Runtime Server - Train'}).status |
 27                        Should be 'Running'
 28                    }
 29
 30                    It "The 'Training System Admin Server' service should be running" {
 31                        (Invoke-Command -Session $Session {Get-Service -Name '*System Admin Server - Train'}).status |
 32                        Should be 'Running'
 33                    }
 34
 35                    It "The 'Training Runtime Server' service should be listening on port 9850" {
 36                        (Test-Port -Computer $Computer -Port 9850).Open |
 37                        Should be 'True'
 38                    }
 39
 40                    It "The 'Training (SSL) Runtime Server' service should be listening on port 9860" {
 41                        (Test-Port -Computer $Computer -Port 9860).Open |
 42                        Should be 'True'
 43                    }
 44
 45                    It "The 'Training System Admin Server' service should be listening on port 4234" {
 46                        (Test-Port -Computer $Computer -Port 4234).Open |
 47                        Should be 'True'
 48                    }
 49
 50                    It "The 'Training (SSL) System Admin Server' service should be listening on port 4235" {
 51                        (Test-Port -Computer $Computer -Port 4235).Open |
 52                        Should be 'True'
 53                    }
 54
 55                }
 56
 57                Context 'Validating the Test environment' {
 58                    It "The 'Test Runtime Server' service should be running" {
 59                        (Invoke-Command -Session $Session {Get-Service -Name '*Runtime Server - Test'}).status |
 60                        Should be 'Running'
 61                    }
 62
 63                    It "The 'Test System Admin Server' service should be running" {
 64                        (Invoke-Command -Session $Session {Get-Service -Name '*System Admin Server - Test'}).status |
 65                        Should be 'Running'
 66                    }
 67
 68                    It "The 'Test Runtime Server' service should be listening on port 9950" {
 69                        (Test-Port -Computer $Computer -Port 9950).Open |
 70                        Should be 'True'
 71                    }
 72
 73                    It "The 'Test (SSL) Runtime Server' service should be listening on port 9960" {
 74                        (Test-Port -Computer $Computer -Port 9960).Open |
 75                        Should be 'True'
 76                    }
 77
 78                    It "The 'Testing System Admin Server' service should be listening on port 3234" {
 79                        (Test-Port -Computer $Computer -Port 3234).Open |
 80                        Should be 'True'
 81                    }
 82
 83                    It "The 'Test (SSL) System Admin Server' service should be listening on port 3235" {
 84                        (Test-Port -Computer $Computer -Port 3235).Open |
 85                        Should be 'True'
 86                    }
 87
 88                }
 89
 90                Context 'Validating the Production environment' {
 91                    It "The 'Production Runtime Server' service should be running" {
 92                        (Invoke-Command -Session $Session {Get-Service -Name '*Runtime Server'}).status |
 93                        Should be 'Running'
 94                    }
 95
 96                    It "The 'Production System Admin Server' service should be running" {
 97                        (Invoke-Command -Session $Session {Get-Service -Name '*System Admin Server'}).status |
 98                        Should be 'Running'
 99                    }
100
101                    It "The 'Production Runtime Server' service should be listening on port 1234" {
102                        (Test-Port -Computer $Computer -Port 1234).Open |
103                        Should be 'True'
104                    }
105
106                    It "The 'Production (SSL) Runtime Server' service should be listening on port 1235" {
107                        (Test-Port -Computer $Computer -Port 1235).Open |
108                        Should be 'True'
109                    }
110
111                    It "The 'Production System Admin Server' service should be listening on port 9750" {
112                        (Test-Port -Computer $Computer -Port 9750).Open |
113                        Should be 'True'
114                    }
115
116                    It "The 'Production (SSL) System Admin Server' service should be listening on port 9760" {
117                        (Test-Port -Computer $Computer -Port 9760).Open |
118                        Should be 'True'
119                    }
120
121                }
122
123                Context 'Validating the Report environment' {
124                    It "The 'Report Runtime Server' service should be running" {
125                        (Invoke-Command -Session $Session {Get-Service -Name '*Runtime Server - Report'}).status |
126                        Should be 'Running'
127                    }
128
129                    It "The 'Report System Admin Server' service should be running" {
130                        (Invoke-Command -Session $Session {Get-Service -Name '*System Admin Server - Report'}).status |
131                        Should be 'Running'
132                    }
133
134                    It "The 'Report Runtime Server' service should be listening on port 9650" {
135                        (Test-Port -Computer $Computer -Port 9650).Open |
136                        Should be 'True'
137                    }
138
139                    It "The 'Report (SSL) Runtime Server' service should be listening on port 9660" {
140                        (Test-Port -Computer $Computer -Port 9660).Open |
141                        Should be 'True'
142                    }
143
144                    It "The 'Report System Admin Server' service should be listening on port 2234" {
145                        (Test-Port -Computer $Computer -Port 2234).Open |
146                        Should be 'True'
147                    }
148
149                    It "The 'Report (SSL) System Admin Server' service should be listening on port 2235" {
150                        (Test-Port -Computer $Computer -Port 2235).Open |
151                        Should be 'True'
152                    }
153
154                }
155
156                Remove-PSSession -Session $Session
157
158            }
159
160        }
161
162    }
163
164}

ovf-splitconfig1a.png

Separating the environmental portion of the test is simple enough and finding items that might need to be changed is now much easier than having all of the environmental specific items buried in one huge test.

 1$ConfigData = @(
 2
 3        @{
 4            Environment = 'Production'
 5            RtServiceName = '*Runtime Server'
 6            RtPort = 9750
 7            RtSSLPort = 9760
 8            AdmServiceName = '*System Admin Server'
 9            AdmPort = 1234
10            AdmSSLPort = 1235
11        }
12        @{
13            Environment = 'Training'
14            RtServiceName = '*Runtime Server - Train'
15            RtPort = 9850
16            RtSSLPort = 9860
17            AdmServiceName = '*System Admin Server - Train'
18            AdmPort = 4234
19            AdmSSLPort = 4235
20        }
21        @{
22            Environment = 'Test'
23            RtServiceName = '*Runtime Server - Test'
24            RtPort = 9950
25            RtSSLPort = 9960
26            AdmServiceName = '*System Admin Server - Test'
27            AdmPort = 3234
28            AdmSSLPort = 3235
29        }
30        @{
31            Environment = 'Report'
32            RtServiceName = '*Runtime Server - Report'
33            RtPort = 9650
34            RtSSLPort = 9660
35            AdmServiceName = '*System Admin Server - Report'
36            AdmPort = 2234
37            AdmSSLPort = 2235
38        }
39
40)

The structural portion of the test itself is now much more condensed and concise along with being reusable:

 1#Requires -Version 3.0 -Modules Pester, MrToolkit
 2function Test-MrAppServer {
 3    [CmdletBinding()]
 4    param (
 5        [Parameter(Mandatory,
 6                   ValueFromPipeline)]
 7        [string[]]$ComputerName,
 8
 9        [hashtable[]]$ConfigurationData,
10
11        [System.Management.Automation.Credential()]$Credential = [System.Management.Automation.PSCredential]::Empty
12    )
13
14    PROCESS {
15        foreach ($Computer in $ComputerName) {
16
17            Describe "Validation of application server: $Computer" {
18                try {
19                    $Session = New-PSSession -ComputerName $Computer -Credential $Credential -ErrorAction Stop
20                }
21                catch {
22                    Write-Warning -Message "Unable to connect. Aborting Pester tests for computer: '$Computer'."
23                    Continue
24                }
25
26                foreach ($Config in $ConfigurationData) {
27
28                    Context "Validating $($Config.Environment) environment" {
29
30                        It "The '$($Config.RtServiceName)' service should be running" {
31                            (Invoke-Command -Session $Session {Get-Service -Name $Using:Config.RtServiceName}).status |
32                            Should be 'Running'
33                        }
34
35                        It "The '$($Config.RtServiceName)' service should be listening on port $($Config.RtPort)" {
36                            (Test-Port -Computer $Computer -Port $Config.RtPort).Open |
37                            Should be 'True'
38                        }
39
40                        It "The '$($Config.RtServiceName) (SSL)' service should be listening on port $($Config.RtSSLPort)" {
41                            (Test-Port -Computer $Computer -Port $Config.RtSSLPort).Open |
42                            Should be 'True'
43                        }
44
45                        It "The '$($Config.AdmServiceName)' service should be running" {
46                            (Invoke-Command -Session $Session {Get-Service -Name $Using:Config.AdmServiceName}).status |
47                            Should be 'Running'
48                        }
49
50                        It "The '$($Config.AdmServiceName)' service should be listening on port $($Config.AdmPort)" {
51                            (Test-Port -Computer $Computer -Port $Config.AdmPort).Open |
52                            Should be 'True'
53                        }
54
55                        It "The '$($Config.AdmServiceName) (SSL)' service should be listening on port $($Config.AdmSSLPort)" {
56                            (Test-Port -Computer $Computer -Port $Config.AdmSSLPort).Open |
57                            Should be 'True'
58                        }
59
60                    }
61
62                }
63
64                Remove-PSSession -Session $Session
65
66            }
67
68        }
69
70    }
71
72}

ovf-splitconfig2a.png

Specific modifications have been made to the code shown in these examples so that the actual application and service names aren't disclosed.

Since I haven't seen anyone else separate environmental code from structural code for operational testing before, I would like to know what your thoughts are on this subject and if you can think of any problems that it may create that I may not have considered.

µ