{"id":81329,"date":"2018-10-15T15:57:51","date_gmt":"2018-10-15T15:57:51","guid":{"rendered":"https:\/\/www.red-gate.com\/simple-talk\/?p=81329"},"modified":"2019-01-22T16:33:29","modified_gmt":"2019-01-22T16:33:29","slug":"testing-powershell-modules-with-pester","status":"publish","type":"post","link":"https:\/\/www.red-gate.com\/simple-talk\/sysadmin\/powershell\/testing-powershell-modules-with-pester\/","title":{"rendered":"Testing PowerShell Modules with Pester"},"content":{"rendered":"<p><strong>The series so far:<\/strong><\/p>\n<ol>\n<li><a href=\"https:\/\/www.red-gate.com\/simple-talk\/sysadmin\/powershell\/introduction-to-testing-your-powershell-code-with-pester\/\">Introduction to Testing Your PowerShell Code with Pester<\/a><\/li>\n<li><a href=\"https:\/\/www.red-gate.com\/simple-talk\/sysadmin\/powershell\/advanced-testing-of-your-powershell-code-with-pester\/\">Advanced Testing of Your PowerShell Code with Pester<\/a><\/li>\n<li><a href=\"https:\/\/www.red-gate.com\/simple-talk\/sysadmin\/powershell\/testing-powershell-modules-with-pester\/\">Testing PowerShell Modules with Pester<\/a><\/li>\n<\/ol>\n\n<p>In the <a href=\"https:\/\/www.red-gate.com\/simple-talk\/sysadmin\/powershell\/introduction-to-testing-your-powershell-code-with-pester\/\">first article<\/a> of this series, I introduced testing PowerShell code with Pester, covering the use of the <code>Describe<\/code>, <code>It<\/code> and <code>Should<\/code> functions. The <a href=\"https:\/\/www.red-gate.com\/simple-talk\/sysadmin\/powershell\/advanced-testing-of-your-powershell-code-with-pester\/\">second article<\/a> built on this knowledge to show the use of mocks, the test drive folder, and showed examples of what unit, integration, and acceptance tests may look like. In this article I\u2019ll build on that to show how to match tests with requirements, and how to properly test a PowerShell module.<\/p>\n<p>Before I begin, I do want to mention a few things. First, I\u2019m going to assume you are comfortable with module development, so I won\u2019t delve into details like the difference in PSM1 and PSD1 files. Second, I\u2019ll assume you\u2019ve read the previous two articles on Pester and have already installed Pester on your system. Finally, there is a lot of code for the complete project, too much to attempt to reprint. In order to get full value from this article you will want to download the demo project from my <a href=\"https:\/\/github.com\/arcanecode\/PowerShell\/tree\/master\/Pester-Module\">github site<\/a>. There is a zipped file that you can download to avoid dealing with individual files.<\/p>\n<h2>Requirements<\/h2>\n<p>In my previous article, I stressed how important requirements were to the success of a testing endeavor. For this article, you are going to create a fictitious scenario. You work for a company called PodcastSight, which aggregates podcast feeds from around the world. For each podcast you create a PowerShell module to manage that particular podcast. The module should read the list of available episodes via its RSS feed, and download both the episode art and the episode itself. In addition, the module should create an HTML webpage with the current RSS feed data, as well as an XML file that will be used by other components within the fictitious PodcastSight company. You have been given a set of requirements to develop this module.<\/p>\n<p>Each module should be named <em>Podcast-podcastname<\/em>. For this demo, you will be using a podcast called <a href=\"http:\/\/noagendashow.com\">No Agenda<\/a> (<a href=\"http:\/\/noagendashow.com\">http:\/\/noagendashow.com<\/a>). This podcast has a license for unrestricted use, in other words, it can be used however you wish without concern for legal issues. In addition, each episode has new podcast art, making that part of the demo much more interesting. Therefore, the first requirement dictates the module be named <em>Podcast-NoAgenda<\/em>. In addition, requirements dictate the module must have both a PSM1 and PSD1 file.<\/p>\n<p>There are also structural requirements, ones that dictate how the module should be assembled.<\/p>\n<ul>\n<li>The code for each function should be in its own PS1 file.<\/li>\n<li>Each file with a function must begin with the name function-, then the name of the function, with of course a ps1 extension.<\/li>\n<li>Each function must have its own corresponding test file.<\/li>\n<li>Each function must have a help section, which includes the synopsis, description, and at least one example.<\/li>\n<li>All functions must contain at least one Write-Verbose statement, letting the caller know the function is executing.<\/li>\n<li>Functions must be coded as advanced functions. They need a clearly defined parameter block, and must be pipeline enabled where it makes sense.<\/li>\n<\/ul>\n<p>Now you come to the requirements that implement the functionality for the module. Below is a list of functions the designer has created, and their purpose. The first two are to be implemented as <em>public<\/em> functions, meaning they can be called outside the module. The remaining functions are <em>private<\/em>, meaning they can only be called from within the module itself.<\/p>\n<p><strong>Get-NoAgenda <\/strong>\u2013 This is the master function. Calling it will execute all of the other functions needed to download the episodes and artwork, as well as create the XML and HTML files.<\/p>\n<p><strong>Get-PodcastData<\/strong> \u2013 This function will call the RSS feed and then parse the data, putting into an array of custom objects you will refer to as <em>Podcast Data<\/em>. The output of this function, Podcast Data, will be used to feed all of the other functions.<\/p>\n<p><strong>Get-PodcastImage<\/strong> \u2013 For this function you pass in the Podcast Data. It will then download the artwork for each episode into a location specified by the user, or a default location.<\/p>\n<p><strong>Get-PodcastMedia<\/strong> \u2013 This function will take the Podcast Data and download each episode; the files from NoAgenda are in MP3 format.<\/p>\n<p><strong>ConvertTo-PodcastXML<\/strong> \u2013 Will take the Podcast Data array and generate an XML structure needed by PodcastSight.<\/p>\n<p><strong>Write-PodcastXML<\/strong> \u2013 This function takes the output of <code>ConvertTo-PodcastXML<\/code> and writes it to a disk location specified by the user or uses the default location defined in the parameters.<\/p>\n<p><strong>ConvertTo-PodcastHTML<\/strong> \u2013 Takes the Podcast Data array and generates an HTML page that could be used on the PodcastSight website.<\/p>\n<p><strong>Write-PodcastHTML<\/strong> \u2013 This takes the output of <code>ConvertTo-PodcastHTML<\/code> and writes it to either the default location defined in the parameters or a disk location specified by the user.<\/p>\n<p>Each function should have a set of tests to ensure they meet the basic requirements. In addition, there should be tests for the structure of the module, i.e., ensuring help is present, there are Write-Verbose, and the like. PodcastSight allows the Acceptance tests to also serve as Integration tests, so it is only necessary to author Unit tests and Acceptance tests.<\/p>\n<div class=\"note\">\n<p><em><strong>A Note on Requirements Documents<\/strong> By no means should you consider the above section a detailed requirements document. A true requirements document would be much more detailed and many pages long. It would include an explicit list of parameters for each function, data types, return data types, and more. This was just to provide a quick overview of requirements for this article. <\/em><\/p>\n<\/div>\n<h2>The Demo<\/h2>\n<p>You will find the <a href=\"https:\/\/github.com\/arcanecode\/PowerShell\/tree\/master\/Pester-Module\">demo for this article<\/a> on my GitHub site. As with my previous articles, I\u2019ll be using my <em>C:\\PowerShell<\/em> folder to store them locally, for this article they will go into <em>C:\\PowerShell\\Pester-Module<\/em>. Within this folder you will download the module sample and place it into <em>C:\\PowerShell\\Pester-Module\\Podcast-NoAgenda<\/em>. Before you attempt to run any code, you\u2019ll also need to create two additional folders in <em>C:\\PowerShell\\Pester-Module<\/em>, <em>Podcast-Data<\/em> and <em>Podcast-Test<\/em>. The first folder is where files will go to by default when they are downloaded. The latter folder is used by the tests as a temporary location during their execution.<\/p>\n<p>Next, there are two files you will want to place directly in <em>C:\\PowerShell\\Pester-Module<\/em>. The first is <em>Execute-Tests.ps1<\/em>. This has code you can use to execute each individual test, or all of the tests at once if you desire. The second is <em>DeployModule.ps1<\/em>. It will take your module and deploy it to your PowerShell folder inside your Documents folder so you can call the module directly without having to explicitly load it from a folder location. As a break down of it would make this article too long, I have a <a href=\"https:\/\/arcanecode.com\/2015\/09\/26\/easy-installation-of-your-own-powershell-modules\/\">complete writeup<\/a> on my blog.<\/p>\n<h2>Testing the Structure<\/h2>\n<p>The first set of tests are focused on the structure of the module. Does it have all the required files? Does each file have the needed components? Those sorts of things will be validated in the structure tests.<\/p>\n<p>For each example in this article I\u2019ll list the code, and will include line numbers to make it easy to reference in the description. If you choose to copy and paste code from here be sure to remove any line numbers, or even better just download the samples which have the line numbers removed.<\/p>\n<p>The structure tests can be found in the <em>Podcast-NoAgenda.Module.Tests.ps1<\/em> file.<\/p>\n<pre class=\"lang:ps theme:powershell-ise\">01: $here = Split-Path -Parent $MyInvocation.MyCommand.Path\r\n02: \r\n03: $module = 'Podcast-NoAgenda'\r\n04: \r\n05: Describe -Tags ('Unit', 'Acceptance') \"$module Module Tests\"  {\r\n06: \r\n07:   Context 'Module Setup' {\r\n08:     It \"has the root module $module.psm1\" {\r\n09:       \"$here\\$module.psm1\" | Should -Exist\r\n10:     }\r\n11: \r\n12:     It \"has the a manifest file of $module.psd1\" {\r\n13:       \"$here\\$module.psd1\" | Should -Exist\r\n14:       \"$here\\$module.psd1\" | Should -FileContentMatch \"$module.psm1\"\r\n15:     }\r\n16:     \r\n17:     It \"$module folder has functions\" {\r\n18:       \"$here\\function-*.ps1\" | Should -Exist\r\n19:     }\r\n20: \r\n21:     It \"$module is valid PowerShell code\" {\r\n22:       $psFile = Get-Content -Path \"$here\\$module.psm1\" `\r\n23:                             -ErrorAction Stop\r\n24:       $errors = $null\r\n25:       $null = [System.Management.Automation.PSParser]::Tokenize($psFile, [ref]$errors)\r\n26:       $errors.Count | Should -Be 0\r\n27:     }\r\n28: \r\n29:   } # Context 'Module Setup'\r\n30: \r\n31: \r\n32:   $functions = ('Get-NoAgenda',\r\n33:                 'Get-PodcastData',\r\n34:                 'Get-PodcastMedia',\r\n35:                 'Get-PodcastImage',\r\n36:                 'ConvertTo-PodcastHtml',\r\n37:                 'ConvertTo-PodcastXML',\r\n38:                 'Write-PodcastHtml', \r\n39:                 'Write-PodcastXML'\r\n40:                )\r\n41: \r\n42:   foreach ($function in $functions)\r\n43:   {\r\n44:   \r\n45:     Context \"Test Function $function\" {\r\n46:       \r\n47:       It \"$function.ps1 should exist\" {\r\n48:         \"$here\\function-$function.ps1\" | Should -Exist\r\n49:       }\r\n50:     \r\n51:       It \"$function.ps1 should have help block\" {\r\n52:         \"$here\\function-$function.ps1\" | Should -FileContentMatch '&lt;#'\r\n53:         \"$here\\function-$function.ps1\" | Should -FileContentMatch '#&gt;'\r\n54:       }\r\n55: \r\n56:       It \"$function.ps1 should have a SYNOPSIS section in the help block\" {\r\n57:         \"$here\\function-$function.ps1\" | Should -FileContentMatch '.SYNOPSIS'\r\n58:       }\r\n59:     \r\n60:       It \"$function.ps1 should have a DESCRIPTION section in the help block\" {\r\n61:         \"$here\\function-$function.ps1\" | Should -FileContentMatch '.DESCRIPTION'\r\n62:       }\r\n63: \r\n64:       It \"$function.ps1 should have a EXAMPLE section in the help block\" {\r\n65:         \"$here\\function-$function.ps1\" | Should -FileContentMatch '.EXAMPLE'\r\n66:       }\r\n67:     \r\n68:       It \"$function.ps1 should be an advanced function\" {\r\n69:         \"$here\\function-$function.ps1\" | Should -FileContentMatch 'function'\r\n70:         \"$here\\function-$function.ps1\" | Should -FileContentMatch 'cmdletbinding'\r\n71:         \"$here\\function-$function.ps1\" | Should -FileContentMatch 'param'\r\n72:       }\r\n73:       \r\n74:       It \"$function.ps1 should contain Write-Verbose blocks\" {\r\n75:         \"$here\\function-$function.ps1\" | Should -FileContentMatch 'Write-Verbose'\r\n76:       }\r\n77:     \r\n78:       It \"$function.ps1 is valid PowerShell code\" {\r\n79:         $psFile = Get-Content -Path \"$here\\function-$function.ps1\" `\r\n80:                               -ErrorAction Stop\r\n81:         $errors = $null\r\n82:         $null = [System.Management.Automation.PSParser]::Tokenize($psFile, [ref]$errors)\r\n83:         $errors.Count | Should -Be 0\r\n84:       }\r\n85: \r\n86:     \r\n87:     } # Context \"Test Function $function\"\r\n88: \r\n89:     Context \"$function has tests\" {\r\n90:       It \"function-$($function).Tests.ps1 should exist\" {\r\n91:         \"$here\\function-$($function).Tests.ps1\" | Should -Exist\r\n92:       }\r\n93:     }\r\n94:   \r\n95:   } # foreach ($function in $functions)\r\n96: \r\n97: }<\/pre>\n<p>Line 1 contains something you\u2019ll see common to most tests. The test needs to determine the folder it is running in; for that you can reference the built-in <code>$MyInvocation <\/code>variable. This has information about the script PowerShell is currently executing. One of the properties is <code>MyCommand<\/code> which has the full path and file name being run. Here you can just reference its <code>Path<\/code> property to get the folder name and place it in a variable named <code>$here<\/code>.<\/p>\n<p>In line 3, the name of the module being tested is placed into a variable. I have found the structural requirements for testing modules to be fairly consistent across modules, by placing the module name into a variable, it allows for greater reuse. This test could simply be copied and used with another module with a quick update to this variable, and one other which will be mentioned shortly.<\/p>\n<p>Line 5 begins the <code>Describe<\/code> block for this test. With this, the same test can be used for both Unit and Acceptance tests, so both have been placed as tags. The name for the describe block is found next. Note how you can leverage the power of PowerShell\u2019s string interpolation to generate the name using the variable.<\/p>\n<p>This test script is a bit long so it was broken into logical parts using Context blocks, the first of which begins at line 7.<\/p>\n<p>Line 8 has the first test, to ensure the module\u2019s PSM file exists. This may seem a bit odd, but recall from a previous article the concept of Test Driven Development (TDD). With TDD, you write all of your tests first, then begin to author your actual code. Running this test without having written any of the modules code will help you keep track of what has been done and what still remains.<\/p>\n<p>In line 12, there is another <code>It<\/code> block, and it actually contains two tests. The first test on line 13 simply checks to see if the manifest file exists. In line 14, you must make sure the manifest contains a correct reference to the module. Often people will copy over manifest files from another project to use as a starting point. The <code>-FileContentMatch<\/code> switch will look inside the PSM1 file to ensure the module name exists inside the manifest, helpful in case you had copied from another project and forgotten to update it with the correct module name.<\/p>\n<p>Line 17 validates that at least one function exists. In a moment you will look for the existence of specific functions, but for now this test will ensure at least one has been created.<\/p>\n<p>Line 21 contains an interesting test. Is the code in the module valid, in other words are there any syntax errors? You\u2019ve often heard PowerShell is built on .NET. The specific set of libraries for PowerShell can be found in <code>System.Management.Automation<\/code>. One of the many classes it contains is <code>PSParser<\/code>, which has a static method named <code>Tokenize<\/code>. Static methods can be executed without having to generate an object from them. You do so by using the syntax shown here, the name of the class, followed by two colons, and finally the method to execute.<\/p>\n<p>The <code>Tokenize<\/code> function needs two parameters. The first is the code to be analyzed. You get the code in lines 22 \u2013 23, using <code>Get-Content<\/code> to load it in memory, and if there was an error you stop the test. The second parameter is actually an output parameter, an array of errors it finds. First then you\u2019ll have to create a variable to hold the output; this is done in line 24. The variable can be empty, hence you can set it to null, but it does have to exist in PowerShell\u2019s scope prior to calling the <code>Tokenize<\/code> method.<\/p>\n<p>Line 25 makes the call to <code>Tokenize<\/code>. Note the use of <code>[ref]<\/code> before the <code>$errors<\/code> variable name, this will pass the memory address of the <code>$errors<\/code> variable so it can be updated.<\/p>\n<p>Finally, line 26 accesses the count property of the <code>$errors<\/code> array. If it is zero, there are no syntax errors and you are good to go. Otherwise this test fails. You could go further and display the contents of <code>$error<\/code> but for this simple test, this will be sufficient. The test for valid PowerShell code will close out the beginning context block.<\/p>\n<p>Beginning in line 32, an array is loaded with the names of the module\u2019s functions. This is the second variable you\u2019d need to change when copying this script to test another module. Following this, on line 42, you begin a <code>foreach<\/code> loop which will iterate over each function name in the array.<\/p>\n<p>Line 45 declares a context block. By doing it inside the loop, you generate a context block for each individual function. This makes the output much easier to read. The first test, on line 47, simply checks to see if the file exists yet. Remember the requirement that the file name for each function begins with <code>function-<\/code> followed by the name of the function.<\/p>\n<p>Lines 51 to 66 represent the checks for help text. It uses the <code>Should<\/code> functions <code>-FileContentMatch<\/code> to ensure the text needed for a help section is present. Contrast this section with the code in lines 68 to 71. In that area you are validating that the keywords to make this function an advanced function are present. All three checks were done in a single <code>It<\/code> block.<\/p>\n<p>What is the advantage of one over the other? With the second example, all three of the checks have to pass for the advanced function test to pass. If any of the three fail, the advanced function will fail, but you may not necessarily know which of the conditions caused it to fail. By breaking it up into individual tests, as you did with the test for help text, you will know exactly which condition failed to meet requirements.<\/p>\n<p>Which way you go is up to you. For this test, I decided if the function test failed, it would be fairly easy to diagnose as the function declaration is easy to find in source code with the <code>cmdletbinding<\/code> and <code>param<\/code> blocks close by. Help text on the other hand can get long, and it may not be as easy to determine which of the required components were missing, hence the decision to break them into individual tests. Again, you will have to evaluate your situation and decide which conditions to group together inside your <code>It<\/code> blocks.<\/p>\n<p>The remainder of the test is pretty straightforward. In line 74, a check to validate the code has at least one <code>Write-Verbose<\/code> block is made. Then in line 78, the <code>Tokenizer<\/code> validates the function is valid PowerShell code, in other words, there are no syntax errors.<\/p>\n<p>Line 89 wraps up the structural tests on line 89 with one final context block that validates that the function has a corresponding tests file.<\/p>\n<h2>Get-PodcastData \u2013 The Function<\/h2>\n<p>For this article you\u2019ll select two functions which will exemplify the type of code you\u2019ll see when authoring function tests. The first of these is the <code>Get-PodcastData<\/code> function. As you can see, it is fairly simple.<\/p>\n<pre class=\"lang:ps theme:powershell-ise\">01: function Get-PodcastData()\r\n02: {\r\n03:   [CmdletBinding()]\r\n04:   param\r\n05:   (\r\n06:     [parameter (Mandatory = $false) ]\r\n07:     [string] $rssFeed = 'http:\/\/feed.nashownotes.com\/rss.xml'\r\n08:   )\r\n09: \r\n10:   Write-Verbose \"Get-PodcastData: Getting RSS Data from $rssfeed\"\r\n11:   \r\n12:   $webData = Invoke-RestMethod $rssFeed\r\n13: \r\n14:   # Use Select-Object to take each object returned and convert it to\r\n15:   # custom objects. Name will become the property name of the new object,\r\n16:   # Expression is the value we are extracting from the returned web data\r\n17:   $rssData = @()\r\n18:   foreach($rss in $webData)\r\n19:   {\r\n20:     # Hosts needs extra cleanup. Originally it came in the feed as a\r\n21:     # string, now it seems to be coming in as an array\r\n22:     if ($rss.author.GetType() -eq 'string')\r\n23:     {\r\n24:       $hosts = $rss.author\r\n25:     }\r\n26:     else\r\n27:     {\r\n28:       $hosts = $rss.author[0]\r\n29:     }\r\n30:     $hosts = $hosts.Replace('&amp;', 'and') # &amp; messes up XML\r\n31: \r\n32:     $rssData += [PSCustomObject][Ordered]@{\r\n33:       # Note the &amp; will mess up the XML, so we'll replace it\r\n34:       PSTypeName = 'PodcastSight.Podcast'\r\n35:       Title = $rss.title.Replace('&amp;', 'and')\r\n36:       ShowUrl = $rss.link\r\n37:       EmbeddedHTML = $rss.summary\r\n38:       Hosts = $hosts\r\n39:       PublicationDate = $rss.pubDate\r\n40:       ImageUrl = $rss.image.href\r\n41:       AudioUrl = $rss.enclosure.url\r\n42:       AudioLength = $rss.enclosure.length \r\n43:     }\r\n44:   }\r\n45:  \r\n46:   return $rssData\r\n47: \r\n48: }<\/pre>\n<p>Lines 1 to 8 are the function declaration. You have one parameter, which defaults to the RSS feed URL for the show. This allows you to overwrite it should the feed ever get moved, but PodcastSight doesn\u2019t wish to make changes to the code. Line 10 has the required <code>Write-Verbose<\/code> statement.<\/p>\n<p>Line 12 is the heart of the function; it uses <code>Invoke-RestMethod<\/code> to read the RSS feed and return it as a blob of XML data. It\u2019s a bit difficult to use XML effectively, so in the next section you convert it to a custom object.<\/p>\n<p>Line 17 declares an empty array variable; this will hold the output of the cleanup routine. Line 18 begins a loop that iterates over each row in the XML web data.<\/p>\n<p>Within lines 22 to 30, an issue specific to the No Agenda podcast is addressed. Originally the RSS feed brought back the authors as a single string. At some point, however, it was modified to return an array. In this block of code, you determine which it is, and use the appropriate syntax to copy it into the <code>$hosts <\/code>variable. As long as you\u2019re working with this variable, you replace any ampersand (&amp;) characters with the word \u2018and\u2019 as ampersands play havoc with the XML you want to output later.<\/p>\n<p>Next up, you take the XML row of data and convert it to an object, adding it to the array in the process. In line 32 the += will add everything following it as a new item to the <code>$rssData<\/code> array. You then create a hash table, but by prepending it with the <code>[PSCustomObject].<\/code> PowerShell will actually convert the hash table to a custom object. The inclusion of [Ordered] will ensure PowerShell keeps the properties in the order you declare them.<\/p>\n<p>PowerShell allows creating a custom type name instead of being stuck with the <code>PSCustomObject<\/code> default. To do so, you declare a property of <code>PSTypeName<\/code> and assign a value to it. When the object is created, it will then have the type name you give it, as done in line 34.<\/p>\n<p>So now that you\u2019ve seen the function, take a look at the code to test it.<\/p>\n<h2>Get-PodcastData \u2013 The Tests<\/h2>\n<p>As you\u2019ve seen, the Get-PodcastData function is fairly simple. The only thing that you might consider mocking is the <code>Invoke-RestMethod<\/code> cmdlet. However, to mock it you would just return a set of hard coded XML, which would always pass the test, and not be of much value. As such PodcastSight has decided to let the same test work for both Acceptance and Unit testing. Here is the test:<\/p>\n<pre class=\"lang:ps theme:powershell-ise\">01: # Get the path the script is executing from\r\n02: $here = Split-Path -Parent $MyInvocation.MyCommand.Path\r\n03: \r\n04: # If the module is already in memory, remove it\r\n05: Get-Module Podcast-NoAgenda | Remove-Module -Force\r\n06: \r\n07: # Import the module from the local path, not from the users Documents folder\r\n08: Import-Module $here\\Podcast-NoAgenda.psm1 -Force\r\n09: \r\n10: Describe 'Get-PodcastData Tests' {\r\n11:\r\n12:   $rssData = Get-PodcastData\r\n13: \r\n14:   $rowNum = 0\r\n15:   foreach ($podcast in $rssData)\r\n16:   {\r\n17:     $rowNum++\r\n18:     Context \"Podcast $rowNum has the correct properties\" {\r\n19:       # Load an array with the properties we need to look for\r\n20:       $properties = ('Title', 'ShowUrl', 'EmbeddedHTML', 'Hosts', \r\n21:                      'PublicationDate', 'ImageUrl', 'AudioUrl', 'AudioLength')\r\n22:       \r\n23:       foreach ($property in $properties)\r\n24:       { \r\n25:         It \"Podcast $rowNum should have a property of $property\" {\r\n26:           [bool]($podcast.PSObject.Properties.Name -match $property) |\r\n27:             Should -BeTrue\r\n28:         }\r\n29:       }\r\n30:     \r\n31:     } # Context 'Individual Podcast Properties' \r\n32:   } # foreach ($podcast in $rssData)\r\n33: \r\n34:   Context 'Podcast Collection Values' {\r\n35:     It 'should have at least 15 rows' {\r\n36:       $rssData.Count | Should -BeGreaterOrEqual 15\r\n37:     }\r\n38: \r\n39:   } # Context 'Podcast Collection Values'\r\n40: \r\n41:   $rowNum = 0\r\n42:   foreach ($podcast in $rssData)\r\n43:   {\r\n44:     $rowNum++\r\n45:     Context \"Podcast Values for row $rowNum Episode $($podcast.Title)\" {\r\n46:       \r\n47:       It 'ImageUrl should end with .jpg or .png' {\r\n48:         $($podcast.ImageUrl.EndsWith('.jpg')) -or `\r\n49:           $($podcast.ImageUrl.EndsWith('.png')) |\r\n50:           Should -BeTrue\r\n51:       }\r\n52:     \r\n53:       It 'AudioUrl should end with .mp3' {\r\n54:         $podcast.AudioUrl.EndsWith('.mp3') | Should -BeTrue\r\n55:       }\r\n56:     \r\n57:       It 'ShowUrl should contain noagendanotes' {\r\n58:         $podcast.ShowUrl.Contains('noagendanotes') -or $podcast.ShowUrl.Contains('nashownotes') |\r\n59:           Should -BeTrue\r\n60:       }\r\n61:     \r\n62:       It 'Hosts should contain Adam Curry' {\r\n63:         $podcast.Hosts.Contains('Adam Curry') | Should -BeTrue\r\n64:       }\r\n65:     \r\n66:       It 'Hosts should contain John C. Dvorak' {\r\n67:         $podcast.Hosts.Contains('John C. Dvorak') | Should -BeTrue\r\n68:       }\r\n69:     } # Context 'Podcast Values'\r\n70:   } # foreach ($podcast in $rssData)\r\n71: \r\n72: } #Describe 'Get-PodcastData' \r\n <\/pre>\n<p>In line 2, the current location is stored in the <code>$here<\/code> variable, as was done in the structure test.<\/p>\n<p>Lines 5 and 8 are extremely important. Often times during development you make changes, run a test, then make modifications to the code in response to the test results. In that situation, you want to make sure the module in memory is the most current update. Line 5 will remove the module from memory, if it is in memory. Line 8 then loads the module, but it does so from the current folder. By being specific, you avoid the module being loaded from one of the Windows default locations, such as the users Documents folder.<\/p>\n<p>The describe block begins in line 10, then, in line 12, the call to the function you are testing is made. Next, you will look at the contents of the return data to ensure it has the data you need.<\/p>\n<p>Line 14 declares a variable <code>$rowNum<\/code>. This is strictly a matter of convenience. Each time through the foreach loop it\u2019s incremented, then it\u2019s displayed with the results. This way, if a particular row fails a test, it will be easy to find that row in the data.<\/p>\n<p>The first <code>Context<\/code> block begins at line 18. This context block validates that each object in the array has the correct properties. As in a previous demo, by placing it inside the loop, it will generate a context block for each row you examine.<\/p>\n<p>Line 20 loads an array up with the list of properties the podcast objects should have. Line 23 starts a <code>foreach<\/code> loop that will cycle over each property name in the <code>$properties<\/code> array. Each PowerShell based object has a collection called, appropriately enough <code>Properties<\/code>. What you want to do is see if the property name from the collection can be found in the properties of the podcast object. If so, the test passes.<\/p>\n<p>The next test, starting with its context block on line 34, is pretty simple. You know the No Agenda RSS feed has at least 15 podcasts in it. So, you check the <code>Count<\/code> property of the array of podcasts (held in <code>$rssData<\/code>) to make sure it has at least 15 items in it.<\/p>\n<p>The final loop, starting on line 42, will generate a <code>Context<\/code> block for each podcast in the RSS data variable. In this block you will validate the data held inside is valid. For example, you know the images will always be either a JPG or PNG image. Line 47 begins a test to validate the file extensions of the image. The remainder of the tests follow a similar pattern, validating things like the media format and names of the hosts.<\/p>\n<h2>Get-PodcastImage \u2013 The Function<\/h2>\n<p>The second function is a bit more challenging in terms of testing, although not horribly complex in design.<\/p>\n<pre class=\"lang:ps theme:powershell-ise\">01: function Get-PodcastImage()\r\n02: {    \r\n03:   [CmdletBinding()]\r\n04:   param\r\n05:   (\r\n06:     [parameter (Mandatory = $true\r\n07:                , ValueFromPipeline = $true\r\n08:                , ValueFromPipelineByPropertyName = $true\r\n09:                ) \r\n10:     ]\r\n11:     $rssData\r\n12:     ,   \r\n13:     [parameter (Mandatory = $false) ]\r\n14:     [string] $OutputPathFolder = 'C:\\PowerShell\\Pester-Module\\Podcast-Data\\'\r\n15:   )\r\n16: \r\n17:   begin \r\n18:   {\r\n19:     Write-Verbose 'Get-PodcastImage: Starting'\r\n20:   }\r\n21:   \r\n22:   process \r\n23:   {\r\n24:     foreach($podcast in $rssData)\r\n25:     {\r\n26:       $imgFileName = $podcast.ImageUrl.Split('\/')[-1]\r\n27:       $outFileName = \"$($OutputPathFolder)$($imgFileName)\"\r\n28:     \r\n29:       # If the file exists, skip it, otherwise download it    \r\n30:       if ( Test-Path $outFileName )\r\n31:       {\r\n32:         Write-Verbose \"`r`nGet-PodcastImage: Skipping $imgFileName, it already exists as $outFileName`r`n\"\r\n33:       }\r\n34:       else\r\n35:       {      \r\n36:         Write-Verbose \"`r`nGet-PodcastImage: Downloading $imgFileName from $($podcast.ImageUrl) `r`n\"\r\n37:         Invoke-WebRequest $podcast.ImageUrl -OutFile $outFileName\r\n38:         Write-Output $imgfileName \r\n39:       }\r\n40:     \r\n41:     } # foreach($podcast in $rssData)\r\n42:   } # process\r\n43: \r\n44:   end\r\n45:   {\r\n46:     Write-Verbose 'Get-PodcastImage: Ending'\r\n47:   }\r\n48: }<\/pre>\n<p>The first few lines declare the function and add the keywords to make it an advanced function. The function has two parameters, the first is the collection of podcast objects from the RSS feed (retrieved with <code>Get-PodcastData<\/code>). The second parameter is the location to store the images you download. If you opt to locate your samples in a place other than <code>C:\\PowerShell\\Pester-Module<\/code>, you\u2019ll need to update the default output paths throughout the demo code.<\/p>\n<p>Since the function is pipeline enabled, there is a <code>begin<\/code> block found on line 17. In it, you are just calling <code>Write-Verbose<\/code> to let the user know we\u2019ve started the function.<\/p>\n<p>Line 22 begins the <code>process<\/code> block. You immediately fall into a <code>foreach<\/code> loop, iterating over each podcast in the RSS data feed. The first thing you do is break off the image file name from the rest of the URL. In line 27 you take the image file name and append it to the output folder, giving us a destination file name.<\/p>\n<p>Line 30 has an <code>if<\/code> statement which calls <code>Test-Path<\/code> to see if the target file is already there. If so, it skips the download (no sense downloading something that is already present). Otherwise, you use <code>Invoke-WebRequest<\/code> to download the file in line 37. Line 38 simply passes the name of the file back, so the calling code will have a list of the files that were downloaded.<\/p>\n<p>The end block, line 44, just sends a <code>Write-Verbose<\/code> to let the caller know it\u2019s finished then the function ends.<\/p>\n<h2>Get-PodcastImage \u2013 The Unit Tests<\/h2>\n<p>Unlike the previous function, you need to create separate tests for Unit and Acceptance. These are covered individually, but before you look at the tests, you should understand there are some unique challenges to tackle to create an effective Unit test. First, recall that Unit tests should be done in isolation as much as possible. This means you should not call functions that you aren\u2019t testing. Of course, this means the <code>Get-PodcastData<\/code> function, but it also extends to the built in <code>Test-Path<\/code> cmdlet.<\/p>\n<p>The second issue is much subtler. In the test, you will be loading the module, then calling the <code>Get-PodcastImage<\/code> function. If you looked at the modules PSM1, file you may have a hint, but for your convenience here is the important part.<\/p>\n<pre class=\"lang:ps theme:powershell-ise\">01: . $PSScriptRoot\\function-Get-PodcastData.ps1\r\n02: . $PSScriptRoot\\function-Get-PodcastMedia.ps1\r\n03: . $PSScriptRoot\\function-Get-PodcastImage.ps1\r\n04: . $PSScriptRoot\\function-Format-PodcastHtml.ps1\r\n05: . $PSScriptRoot\\function-Format-PodcastXml.ps1\r\n06: . $PSScriptRoot\\function-ConvertTo-PodcastHtml.ps1\r\n07: . $PSScriptRoot\\function-ConvertTo-PodcastXml.ps1\r\n08: . $PSScriptRoot\\function-Write-PodcastHtml.ps1\r\n09: . $PSScriptRoot\\function-Write-PodcastXML.ps1\r\n10: . $PSScriptRoot\\function-Get-NoAgenda.ps1\r\n11: \r\n12: Export-ModuleMember Get-NoAgenda\r\n13: Export-ModuleMember Get-PodcastData<\/pre>\n<p>Lines 1 to 10 simply execute the code in each function to load it in memory. Lines 12 and 13 uses <code>Export-ModuleMember<\/code> on two of the functions. If you are not familiar with it, <code>Export-ModuleMember<\/code> will make the function name passed in visible outside the module, so it can be used. All other functions are private, and only callable from within the module.<\/p>\n<p>But wait, some of you are saying. <code>Get-PodcastImage<\/code> is not exported! Which means you cannot call it from outside the module, so how do you test it?<\/p>\n<p>That\u2019s where the Pester function <code>InModuleScope<\/code> comes into play. Take a look at the test for <code>Get-PodcastImage<\/code>.<\/p>\n<pre class=\"lang:ps theme:powershell-ise\">001: $here = Split-Path -Parent $MyInvocation.MyCommand.Path\r\n002: \r\n003: Get-Module Podcast-NoAgenda | Remove-Module -Force\r\n004: Import-Module $here\\Podcast-NoAgenda.psm1 -Force\r\n005: \r\n006: InModuleScope Podcast-NoAgenda { \r\n007: \r\n008:   Describe 'Get-PodcastImage Unit Tests Parameter' -Tags 'Unit' {\r\n009: \r\n010:   \r\n011:     $mockRssData = @'\r\n012: &lt;Objs Version=\"1.1.0.1\" xmlns=\"http:\/\/schemas.microsoft.com\/powershell\/2004\/04\"&gt;\r\n013:   &lt;Obj RefId=\"0\"&gt;\r\n014:     &lt;TN RefId=\"0\"&gt;\r\n015:       &lt;T&gt;PodcastSight.Podcast&lt;\/T&gt;\r\n016:       &lt;T&gt;System.Management.Automation.PSCustomObject&lt;\/T&gt;\r\n017:       &lt;T&gt;System.Object&lt;\/T&gt;\r\n018:     &lt;\/TN&gt;\r\n019:     &lt;MS&gt;\r\n020:       &lt;S N=\"Title\"&gt;819: non-binary person&lt;\/S&gt;\r\n021:       &lt;S N=\"ShowUrl\"&gt;http:\/\/819.noagendanotes.com\/&lt;\/S&gt;\r\n022:       &lt;S N=\"EmbeddedHTML\"&gt;EMBEDDED HTML GOES HERE&lt;\/S&gt;\r\n023:       &lt;S N=\"Hosts\"&gt;Adam Curry and John C. Dvorak&lt;\/S&gt;\r\n024:       &lt;S N=\"PublicationDate\"&gt;Sun, 24 Apr 2016 20:08:15 GMT&lt;\/S&gt;\r\n025:       &lt;S N=\"ImageUrl\"&gt;http:\/\/adam.curry.com\/enc\/20160424200416_na-819-art-feed.jpg&lt;\/S&gt;\r\n026:       &lt;S N=\"AudioUrl\"&gt;http:\/\/mp3s.nashownotes.com\/NA-819-2016-04-24-Final.mp3&lt;\/S&gt;\r\n027:       &lt;S N=\"AudioLength\"&gt;127171457&lt;\/S&gt;\r\n028:     &lt;\/MS&gt;\r\n029:   &lt;\/Obj&gt;\r\n**** LINES 30 to 211 removed for brevity, please see the downloadable samples for the full text\r\n212: &lt;\/Objs&gt;\r\n213: '@\r\n214: \r\n215:   \r\n216:     $rssData = [System.Management.Automation.PSSerializer]::DeserializeAsList($mockRssData)\r\n217: \r\n218:     &lt;#--------------------------------------------------------------------------------------------------- \r\n219:        For the first set of tests, we will call the function using an empty folder. The first will test \r\n220:        calling using the parameter, the second using a parameter. \r\n221:       \r\n222:        Since these are virtually identical, we'll use a simple loop and call the tests, just altering \r\n223:        the call and the folder name\r\n224:     ---------------------------------------------------------------------------------------------------#&gt;\r\n225: \r\n226:     $loops = 'parameter', 'pipeline'\r\n227:     foreach ($loop in $loops)\r\n228:     { \r\n229:       Context \"Unit Test Get-PodcastImage for each file using the $loop\" {\r\n230:         # Because the TestDrive won't get cleared out between context calls we'll\r\n231:         # just create a subfolder for each test and put the files there\r\n232:         $testDriveFolder = \"$($TestDrive)\\$($loop)\\\"\r\n233:         New-Item $testDriveFolder -ItemType directory\r\n234:         \r\n235:         # The function calls Test-Path, we should Mock it\r\n236:         # Since this is an empty folder, Test-Path should always return false\r\n237:         Mock Test-Path { return $false }\r\n238:         \r\n239:         if ($loop -eq 'parameter')                             # Execute the function using a parameter\r\n240:           { $downloadedImages = Get-PodcastImage -rssData $rssData -OutputPathFolder $testDriveFolder }\r\n241:         else                                                   # Execute the function using the pipeline\r\n242:           { $downloadedImages = $rssData | Get-PodcastImage -OutputPathFolder $testDriveFolder }\r\n243:   \r\n244:         foreach($podcast in $rssData)\r\n245:         {\r\n246:           $imgFileName = $podcast.ImageURL.Split('\/')[-1]\r\n247:           $outFileName = \"$($testDriveFolder)$($imgFileName)\"\r\n248:           It \"Image $imgFileName should exist\" {      \r\n249:             $outFileName | Should Exist\r\n250:           }\r\n251:   \r\n252:           It \"Image $imgFileName should exist in download list\" {\r\n253:             [bool]($imgFileName -in $downloadedImages) | Should -BeTrue\r\n254:           }\r\n255:         } # foreach($podcast in $rssData)\r\n256:   \r\n257:       } # Context \"Unit Test Get-PodcastImage for each file using the $loop\"\r\n258:     } # foreach ($loop in $loops)\r\n259: \r\n260: \r\n261:     &lt;#--------------------------------------------------------------------------------------------------- \r\n262:        In the second set of tests, we will fake an existing file, so that it will trigger the do not \r\n263:        download flag within the function. This will let us know it is correctly skipping over files \r\n264:        to preserve our bandwidth\r\n265:     ---------------------------------------------------------------------------------------------------#&gt;\r\n266:     $loops = 'parameter', 'pipeline'\r\n267:     foreach ($loop in $loops)\r\n268:     { \r\n269:       Context \"Unit Test Get-PodcastImage $loop test with existing files\" {\r\n270:         # Because the TestDrive won't get cleared out between context calls we'll\r\n271:         # just create a subfolder for each test and put the files there\r\n272:         $testDriveFolder = \"$($TestDrive)\\$($loop)Exist\\\"\r\n273:         New-Item $testDriveFolder -ItemType directory\r\n274:      \r\n275:         # For this test, we need to ensure it is not downloading files that\r\n276:         # already exist. To do so, we'll begin by taking the first seven files\r\n277:         # from our mock data and adding them to an array \r\n278:         $existingFiles = @()\r\n279:         for ($x = 0; $x -lt 7; $x += 1) \r\n280:           { $existingFiles += $($rssData[$x].ImageURL.Split('\/')[-1]) }\r\n281:      \r\n282:         &lt;#\r\n283:            Next we need to mock Test-Path, to fake the existance of one or more files. This triggers the\r\n284:            functions do not d\/l me logic so we can test it. \r\n285:            \r\n286:            Note we don't want to actually create files, we just need to have our fake Test-Path tell \r\n287:            the Get-PodcastImage function they exist so it will not download them. We'll use the \r\n288:            non-existance of the file in one of our tests.\r\n289:         #&gt;\r\n290:         Mock Test-Path {\r\n291:           # Note the Mock automatically adds the $path variable based on the\r\n292:           # signature of Test-Path, i.e. its -Path parameter\r\n293:           $fileName = $path.Split('\\')[-1]\r\n294:           if ($fileName -in $existingFiles)\r\n295:             { $retValue = $true }\r\n296:           else\r\n297:             { $retValue = $false }\r\n298:           return $retValue \r\n299:         }\r\n300:      \r\n301:         if ($loop -eq 'parameter')                             # Execute the function using a parameter\r\n302:           { $downloadedImages = Get-PodcastImage -rssData $rssData -OutputPathFolder $testDriveFolder }\r\n303:         else                                                   # Execute the function using the pipeline\r\n304:           { $downloadedImages = $rssData | Get-PodcastImage -OutputPathFolder $testDriveFolder }\r\n305:         \r\n306:         # For the first test, ensure the files the function reported as downloaded actually were\r\n307:         foreach($imageFile in $downloadedImages)\r\n308:         {        \r\n309:           $outFileName = \"$($testDriveFolder)$($imageFile)\"\r\n310:           It \"Image $imageFile have been downloaded, and thus should exist\" {      \r\n311:             $outFileName | Should -Exist\r\n312:           }     \r\n313:         } # foreach($podcast in $rssData)\r\n314:         \r\n315:         # For the files that are supposed to already exist, we'll do two tests\r\n316:         foreach ($imageFile in $existingFiles)\r\n317:         {\r\n318:           # First, we'll make sure the supposedly existing file was not reported as downloaded\r\n319:           It \"$imageFile should not exist in the list of downloaded images\" {\r\n320:             [bool]($imageFile -in $downloadedImages) | Should -BeFalse\r\n321:           }\r\n322:      \r\n323:           # Next, validate the file DOESN'T exist. In otherwords, since it wasn't supposed to download, \r\n324:           # we'll make sure it didn't\r\n325:           $outFileName = \"$($testDriveFolder)$($imageFile)\"\r\n326:           It \"$imageFile should not have been downloaded, and thus should not exist\" {\r\n327:             $outFileName | Should -Not -Exist\r\n328:           }\r\n329:      \r\n330:         }\r\n331:      \r\n332:       } # Context \"Unit Test Get-PodcastImage $loop test with existing files\"\r\n333:     } # foreach ($loop in $loops)\r\n334:     \r\n335:   } # Describe 'Get-PodcastImage Unit Tests'\r\n336: \r\n337: } # InModuleScope Podcast-NoAgenda<\/pre>\n<p>Lines 1 to 4 you\u2019ve seen in the last example; a variable is loaded with the current directory then unload and reload the module fresh. It\u2019s line 6 where the magic occurs. Here, <code>InModuleScope<\/code> is followed by the name of the module you want to change the scope to.<\/p>\n<p>Hopefully, you are familiar with the concept of scopes. If you want a technical refresher you can read more <a href=\"https:\/\/docs.microsoft.com\/en-us\/powershell\/module\/microsoft.powershell.core\/about\/about_scopes?view=powershell-6\">here<\/a>. Briefly, think about scope like rooms in a house. If you are in your living room, and have a glass of water you can see it, drink it, or give some to your cat. If you put the glass down and go into your bedroom, for all practical purposes the glass no longer exists. You can\u2019t see it, touch it, or determine if your cat has drunk all of it.<\/p>\n<p>Scope is similar. You can think of your test as the living room and the module as your bedroom. Any variables you declare in the test cannot be seen from the module, likewise anything declared in a module is not visible outside of it. Well almost.<\/p>\n<p>It is possible to declare things in a special way, such as was done with <code>Export-ModuleMember<\/code> which allows an item such as a function or variable to be visible outside the module. It\u2019s sort of like taking your alarm clock and putting it in the door of your bedroom, then it could be seen from both the living room and the bedroom.<\/p>\n<p><code>InModuleScope<\/code> allows moving (to extend the analogy) from the living room, where you are executing the test, into the bedroom and running the test in there for a while. While you get to use the things in the bedroom, such as the <code>Get-PodcastImage<\/code> function, you lose the ability to see things like the <code>$here<\/code> variable declared on line 1. If, on line 7, you had <code>Write-Host $here<\/code>, it would return nothing as that variable is not visible inside the module.<\/p>\n<p>Line 8 has the <code>Describe<\/code> block. Then starting in line 11, there is an odd variable assignment, <code>$mockRssData<\/code>. The variable declaration begins in line 11, and goes all the way to line 213! (Note most of it has been clipped out for brevity, the download has the full code). So, what in the world is this for?<\/p>\n<p>One of the tenants of unit testing is isolation. You only want to call the function you are testing. But that function is dependent on the output of the <code>Get-PodcastData<\/code> function. For this test, rather than calling it, you will instead use a set of <em>known data<\/em>. By using known data, you have already validated the input data is error free. In addition, you should be able to predict the output based on the input, allowing us better validation of the test results.<\/p>\n<p>The code comments in the download have complete step by step instructions on creating this, but in short, <code>Get-PodcastData<\/code> is called the results are stored in a variable. The results are then saved to a file using the <code>Export-CliXml<\/code> cmdlet. Next, the file is opened, and the contents are pasted into the <code>$mockRssData<\/code> variable.<\/p>\n<p>You may recall how you used the <code>Tokenizer<\/code> method of the <code>System.Management.Automation.PSParser<\/code> class to validate the PowerShell code. Under the hood, the <code>Import-CliXml<\/code> uses another method from <code>System.Management.Automation<\/code>, specifically the <code>DeserialzeAsList<\/code> method of the <code>PSSerializer<\/code> class. This takes a blob of XML that was prepared using <code>Export-CliXml<\/code> and converts it back into an object.<\/p>\n<p>So why not just use <code>Import-CliXml<\/code> and import the file? When testing, you want to ensure you limit external influences as much as possible. The external file might get overwritten, or deleted. In that case the set of known data would vanish, and you would have to carefully debug the test. With the data embedded in the test, you guarantee that the known data set will be preserved and reliable.<\/p>\n<p>In line 216 the static, known data is converted back into a variable which has the same contents as if loaded from <code>Get-PodcastData<\/code>.<\/p>\n<p>When testing you need to test all possible conditions. The <code>Get-PodcastImage<\/code> function can be called in one of two ways, passing in data via the pipeline, or as a parameter so you need to execute it using both methods in the test. Because the code will be very similar for both tests, you can save a lot of repetitive code by using a loop. In line 226, an array is loaded with the values parameter and pipeline. (Any two values could have been used, these just made it easier to understand the required logic in the loop.)<\/p>\n<p>Once in the for loop a context block is generated, on line 229. Then on line 232 you see the test setting up a <code>$testDriveFolder<\/code> variable using a variable called <code>$TestDrive<\/code> followed by the name of the current loop, parameter or pipeline.<\/p>\n<p>I\u2019m not trying to trick you, I haven\u2019t secretly created the <code>$TestDrive<\/code> variable, nor have I carelessly forgotten it. It\u2019s something that Pester thoughtfully provides for us!<\/p>\n<p>Remember, not only are Unit tests supposed to be done in isolation, they aren\u2019t supposed to have any side effects such as leaving files on the hard drive after a test. But downloading files is exactly what <code>Get-PodcastImage<\/code> is supposed to do!<\/p>\n<p>You could of course create a temporary folder name, first making sure it doesn\u2019t exist, create it, then delete everything in it after the test is done, but whew that is a lot of work. Instead, the folks who authored Pester thoughtfully provided a way to achieve this with no effort on your part.<\/p>\n<p>When the Pester module\u2019s <code>Describe<\/code> block is invoked, it creates a temporary directory on your drive called the <em>test drive<\/em>. It\u2019s irrelevant where, and part of the reason for that is you shouldn\u2019t try to access it directly. Instead, Pester places the location of your test drive in a variable named <code>$TestDrive<\/code>.<\/p>\n<p>The rules around the test drive are interesting. As mentioned, part of the execution of the <code>Describe<\/code> function is to setup a <code>$TestDrive<\/code> folder. When the <code>Describe<\/code> ends, it deletes the test drive folder along with its contents. When a <code>Context<\/code> block is executed, things change a little. At the beginning of the <code>Context,<\/code> it will make a copy of the contents of the test drive folder. Your code can then interact with the contents, adding, changing and removing files. When the <code>Context <\/code>block ends, everything in the test drive folder is removed, and the contents are then restored from the copy that was made at the start of the context block.<\/p>\n<p>In line 229 when the <code>Context<\/code> is executed, it makes a copy of the test drive. As nothing has been done with it yet, the test drive is empty. Then line 233 creates new subdirectories, one for each pass in the loop (parameter and pipeline). You need to do this as the <code>$TestDrive<\/code> contents will exist for the duration of the <code>Context<\/code> block, but you need to download the images twice, once when called using parameters and once when called using the pipeline. The simple solution then was to just create two folders to hold the downloaded images.<\/p>\n<p>Within the <code>Get-PodcastImage<\/code>, it uses <code>Test-Path<\/code> to first see if the image already exists before it tries to download it. Remembering the rule not to call anything else but the function you\u2019re actually testing, you want to replace <code>Test-Path<\/code> with your own version. In my <a href=\"https:\/\/www.red-gate.com\/simple-talk\/sysadmin\/powershell\/advanced-testing-of-your-powershell-code-with-pester\/\">previous article<\/a> I covered the use of <em>Mocks <\/em>in some detail so I won\u2019t repeat here. Since <code>$TestDrive<\/code> folder was empty at the start of the <code>Context<\/code>, you know the images won\u2019t be there, so you can just have <code>Test-Path<\/code> return a false every time.<\/p>\n<p>Lines 239 to 242 finally call the <code>Get-PodcastImage<\/code> function. It\u2019s called in two ways, the first calling it passing in <code>$rssData<\/code> as a parameter, the second using <code>$rssData<\/code> as input to <code>Get-PodcastImage<\/code> via the pipeline.<\/p>\n<p>Now that the function has executed, you can test the output. It loops over each podcast in the set of known data (held in <code>$rssData<\/code>) and first sees if the file exists, done in lines 246 to 250.<\/p>\n<p>The <code>Get-PodcastData<\/code> returns a list of every file it downloaded, so in lines 252-254, each image is validated in the known data was found in the output list. The <code>Context<\/code> block ends in line 257, and at that point Pester deletes everything that was put in the test drive folder during the execution of the <code>Context<\/code> block. It then restores what was there at the beginning of the <code>Context<\/code> block, which in this case is nothing as the test drive was empty.<\/p>\n<p>Thus far the tests are for downloading files when the directory is empty, and testing for both the podcast dataset passed in as a parameter and via the pipeline. The <code>Get-PodcastImage<\/code> function has an important component: it tests to see if the file already exists and, if so, it skips it. The file won\u2019t be downloaded, nor will it be in the list of files the function returns as being downloaded. As part of the testing, then, you also need to ensure that functionality is working correctly.<\/p>\n<p>That\u2019s what the next test, beginning in line 266 does. Lines 266 to 273 are a repeat from the last test. For this test, you want to designate some files as already existing. They don\u2019t have to actually exist, you just have to make the <code>Get-PodcastImage <\/code>function think they do, which is accomplished in the mocked version of <code>Test-Path<\/code>. In preparation, you\u2019ll copy over seven file names from the <code>$rssData<\/code> variable into another array called <code>$existingFiles<\/code>, as shown in lines 278 to 280.<\/p>\n<p>Starting in line 290, a mocked version of <code>Test-Path<\/code> is created. It extracts the file name from the parameter being passed in, then checks to see if it is already in the <code>$existingFiles<\/code> array. If so it returns true, otherwise false.<\/p>\n<p>With lines 301 to 304, the <code>Get-PodcastImage<\/code> function is called, passing in <code>$rssData<\/code> as a parameter or via the pipeline depending on which iteration of the loop it\u2019s in. The list of files downloaded are returned and placed in the <code>$downloadedImages<\/code> variable. Remember, this list should exclude the file names stored in the <code>$existingImages<\/code> variable because the mocked <code>Test-Path<\/code> indicated these already existed, so the function would have skipped downloading them as well as not included them in the output list.<\/p>\n<p>The tests generated beginning in line 306 are ones that would seem obvious. You take each file that the function said it downloaded, then check to see if it exists in the test drive.<\/p>\n<p>Line 316 moves into two tests that may not seem quite so obvious to do; you want to test for a negative condition. The first test, in line 319, checks to see if one of the existing files appears in the list of downloaded files. If so, you know it was reported as downloaded in error.<\/p>\n<p>The second test, beginning in line 325, checks to make sure none of the files in the existing files array are in the test folder. If they were, again the test would fail as they shouldn\u2019t be there.<\/p>\n<p>Whew! That was a lot of work! Over 300 lines of code to test a function that was just shy of 50 lines long. But you\u2019ll often find that the tests wind up being much longer than what is being tested. There is setup, creating sets of known data, mocking up functions, calling the function as both a parameter and via a pipeline (if applicable), checking for various conditions such as downloading with and without files already present. And all this is just for the unit test; you still have to handle the Acceptance test! Time to do that next.<\/p>\n<h2>Get-PodcastImage \u2013 The Acceptance Tests<\/h2>\n<p>You will rest a bit easier knowing that Acceptance tests are a lot easier to construct. As you saw with Unit tests, there is a lot of setup creating known datasets, test drive folders, and mocking up functions that are not being tested. Much of that goes away with the Acceptance tests, as you will be using components directly, without having to mock them or worry about temporary storage. Take a look at the Acceptance tests for <code>Get-PodcastImage<\/code>.<\/p>\n<pre class=\"lang:ps theme:powershell-ise\">01: Describe 'Get-PodcastImage Acceptance Tests' -Tags 'Acceptance' {\r\n02: \r\n03:   InModuleScope Podcast-NoAgenda { \r\n04:   \r\n05:     $rssData = Get-PodcastData\r\n06: \r\n07:     $root = 'C:\\PowerShell\\Pester-Module\\'\r\n08:     $tests = @{ 'default parameter' = \"$($root)Podcast-Data\\\"\r\n09:                 'default pipeline'  = \"$($root)Podcast-Data\\\"\r\n10:                 'nondefault parameter' = \"$($root)Podcast-Test\\\"\r\n11:                 'nondefault pipeline'  = \"$($root)Podcast-Test\\\"\r\n12:               }\r\n13: \r\n14:     foreach($test in $tests.GetEnumerator())\r\n15:     {\r\n16:       $folder = $test.Value\r\n17: \r\n18:       # Get a list of images already present\r\n19:       $existingImages = Get-ChildItem \"$($folder)*\" -Include *.jpg, *.png | Select-Object Name\r\n20: \r\n21:       # Call Get-PodcastImage based on which test we are running for\r\n22:       switch ($test.Name)\r\n23:       {\r\n24:         'default parameter'    \r\n25:            { $downloadedImages = Get-PodcastImage -rssData $rssData -Verbose }\r\n26:         'default pipeline'     \r\n27:            { $downloadedImages = $rssData | Get-PodcastImage -Verbose }\r\n28:         'nondefault parameter' \r\n29:            { $downloadedImages = Get-PodcastImage -rssData $rssData -OutputPathFolder $folder -Verbose }\r\n30:         'nondefault pipeline'  \r\n31:            { $downloadedImages = $rssData | Get-PodcastImage -OutputPathFolder $folder -Verbose }\r\n32:       } # switch ($test.Name)\r\n33: \r\n34:       # Use split to reduce the test name to either default or nondefault and parameter or pipeline.\r\n35:       # We'll use them in the context and test names to reduce their length\r\n36:       $dlFolder = $test.Name.Split(' ')[0]\r\n37:       $pipeParam = $test.Name.Split(' ')[1]\r\n38: \r\n39:       Context \"Acceptance Test Get-PodcastImage $pipeParam test to $dlFolder folder\" {\r\n40:         # Make sure all the files exist\r\n41:         foreach($podcast in $rssData)\r\n42:         {\r\n43:           $imgFileName = $podcast.ImageURL.Split('\/')[-1]\r\n44:           $outFileName = \"$($folder)$($imgFileName)\"\r\n45:           It \"image $imgFileName should exist in the $dlFolder folder\" {      \r\n46:             $outFileName | Should -Exist\r\n47:           }\r\n48:         }\r\n49:       \r\n50:         # Make sure downloaded images weren't in the list of existing ones\r\n51:         foreach ($img in $downloadedImages)\r\n52:         {\r\n53:           It \"should have downloaded $img\" {\r\n54:             [bool]($img -in $existingImages) | Should -BeFalse\r\n55:           }\r\n56:         }\r\n57:       \r\n58:       } # Context 'Acceptance Test Get-PodcastImage \r\n59: \r\n60:       # Optional: Remove the images that were just downloaded so we can reset for next test\r\n61:       # foreach ($img in $downloadedImages)\r\n62:       # {\r\n63:       #   Remove-Item \"$($folder)$($img)\" -ErrorAction SilentlyContinue\r\n64:       # }\r\n65:           \r\n66:     } # foreach($test in $tests.GetEnumerator())\r\n67: \r\n68:   } # InModuleScope Podcast-NoAgenda\r\n69: \r\n70: } # Describe 'Get-PodcastImage Acceptance Tests'<\/pre>\n<p>Line 1 kicks off with the <code>Describe<\/code> block, then on line 3, you have the <code>InModuleScope<\/code> declaration.<\/p>\n<p>Before you go zooming past line 3, an important point needs to be made. If you scroll up to the Unit test, you\u2019ll see that the <code>Describe<\/code> block was declared <em>inside<\/em> the <code>InModuleScope<\/code>. In this test, you\u2019re doing exactly the opposite. In fact, <code>InModuleScope<\/code> is extremely flexible in that you can declare it at any level you need. You could place it inside <code>Context<\/code> blocks or place a <code>Context<\/code> block in it. You could even get microscopic, and only put the call to <code>Get-PodcastImage<\/code> inside the <code>InModuleScope<\/code> and leave everything else in the scope of the test.<\/p>\n<p>It\u2019s important to understand this flexibility with <code>InModuleScope<\/code> when creating your tests. For the tests in the examples the scoping hasn\u2019t had a significant impact, but you may have situations where you need to repeatedly shift scope back and forth between your module and your tests. Just remember <code>InModuleScope<\/code> is extremely useful, and think about how scope will apply to your tests when authoring them.<\/p>\n<p>Line 5 executes the call to <code>Get-PodcastData <\/code>and returns the data to the <code>$rssData<\/code> variable. The unit tests worked with a set of known values. For these tests, you are working with the current, live values. This is both a risk and a benefit. If the folks at No Agenda changed something in their RSS feed you didn\u2019t anticipate, it could cause the test to fail. You would need to evaluate the reason for the failure and update the tests (and possibly the module\u2019s code) accordingly.<\/p>\n<p>For the exact same reason though, it\u2019s a benefit. No Agenda releases new episodes every Thursday and Sunday. Before they attempt to download the episode each week, the employees at the fictitious company PodcastSight could run the test suite. If they all pass, you know you are clear to download the latest episode. If they fail, you can investigate and correct any issues before you attempt to download the episodes into the production environment.<\/p>\n<p>As you are downloading things for real, and not to a test drive environment, you need to set up a series of folders to hold the downloads. The root folder is assigned in line 7. Then in lines 8 to 12 a hash table is created. The left side of the hash is the test you are doing; the right is the download path. Note, to keep this code simple <em>the test assumes these folders already exist<\/em>! If you have downloaded the samples, be sure to check to see if the files are in place. When coding your tests, you may wish to include a few extra lines to see if the folders exist and if not create them.<\/p>\n<p>Additionally, there was no <em>technical<\/em> reason you couldn\u2019t have used the <code>$TestDrive<\/code> folder. With Acceptance tests though, you typically want to leave the results of your tests, in this case the downloaded podcast images, available for the testers to see and review after the tests are executed. It is for this reason then you chose to save the images to a folder where they could be persisted after the tests have completed running.<\/p>\n<p>Beginning in line 14, a <code>foreach<\/code> loop enumerates over the hash table. You can easily iterate over an array by just using the name of the array after the <code>in<\/code> clause of the <code>foreach<\/code>. Hash tables though work a bit differently. With a hash table you have to use its <code>GetEnumerator()<\/code> method to \u201cpop\u201d the next item off the hash table and copy it into the placeholder variable (in this case <code>$test<\/code>).<\/p>\n<p>Once inside the <code>foreach<\/code>, you can access the key\/value pair of the hash table through the placeholder. The <code>Value<\/code> property refers to the data in the right side of the hash table. The <code>Name<\/code> property corresponds to the key, the data on the left side of the hash table assignments.<\/p>\n<p>Because <code>$test.Value<\/code> isn\u2019t overly clear, in line 16, it\u2019s copied into a variable named <code>$folder<\/code>. Not only is it more concise, it clearly conveys what the contents of the variable are.<\/p>\n<p>For these tests, you need to call the <code>Get-PodcastImage<\/code> with four different combinations, first calling it passing in <code>$rssData<\/code> via the pipeline, then as a parameter. For each one of these, you want to test using the default download folder as well as passing in a value for the download folder. That\u2019s what switch statement that begins in line 22 does. As the code loops over the hash table, it determines which test is the current one from the hash tables key (<code>$test.Name<\/code>).<\/p>\n<p>One of the design principals of Pester is readability. As such you really want the test output to be readable as well. Line 36 uses the split method of a string to break the hash table\u2019s key into two parts. The first part (default or nondefault) will be copied into the <code>$dlFolder<\/code> variable. In line 37, it\u2019s repeated, only this time it copies over the pipeline or parameter word into the <code>$pipeParam<\/code> variable.<\/p>\n<p>The <code>Context<\/code> block beginning on line 39 is used to make a clearly readable name for the testers to see. They are reused throughout the <code>Context<\/code> block to improve readability in the test output (line 45), as well as in constructing path names (line 44).<\/p>\n<p>Speaking of the <code>Context<\/code> block, the first set of tests in it begins on line 41. Each image in the <code>$rssData<\/code> should exist in the target folder. It doesn\u2019t really matter if the image was downloaded during the test or already existed, either way the image should exist which is the important part.<\/p>\n<p>Next though, you will test to ensure that the list of downloaded images wasn\u2019t in the list of images that already existed. In other words, did it correctly skip the images that already existed and only download the ones it needed? That\u2019s the job of the test in lines 51 to 56.<\/p>\n<p>This wraps up the testing. There is an optional block beginning at line 60. It\u2019s commented out, but you may have some circumstances where you want to delete what was just downloaded so you can rerun the test. In that case this code could be uncommented to remove any images that were just downloaded. Note this only deletes the images that were just downloaded during this test, anything other files already in the folder are left alone.<\/p>\n<h2>Execute-Tests.ps1<\/h2>\n<p>Before you wrap things up, your attention should be called to a file in the downloads called Execute-Tests.ps1. This script has been written with individual statements to execute each Pester test in the collection. Using it you can execute the tests individually as you learn. There is also a spot where you can execute all of the tests at once.<\/p>\n<h2>Summary<\/h2>\n<p>Thanks for sticking with it through this rather long article. If you\u2019ve looked over the demo code, you\u2019ll see far more than what is covered here. There are many examples that may aid you in the future, so be sure to look over the code base. It is well documented throughout.<\/p>\n<p>As you can see, Pester is an incredibly powerful tool. Used well, it can aid you in producing high quality, bug free code. It doesn\u2019t have to stop with code either, you can use Pester to verify a wide variety of things. You could write Pester tests to validate your SQL Server databases are current. Is you server configured correctly? Write a Pester test that you can run regularly to validate it. The possibilities are endless!<\/p>\n","protected":false},"excerpt":{"rendered":"<p>In the third article of this series on testing PowerShell code with Pester, Robert Cain demonstrates how to test the functions in a PowerShell module. &hellip;<\/p>\n","protected":false},"author":316962,"featured_media":0,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"footnotes":""},"categories":[53,35],"tags":[95506],"coauthors":[52865],"class_list":["post-81329","post","type-post","status-publish","format-standard","hentry","category-featured","category-powershell","tag-automate"],"acf":[],"_links":{"self":[{"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/posts\/81329","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/users\/316962"}],"replies":[{"embeddable":true,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/comments?post=81329"}],"version-history":[{"count":9,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/posts\/81329\/revisions"}],"predecessor-version":[{"id":81350,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/posts\/81329\/revisions\/81350"}],"wp:attachment":[{"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/media?parent=81329"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/categories?post=81329"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/tags?post=81329"},{"taxonomy":"author","embeddable":true,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/coauthors?post=81329"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}