Use Docker to automate testing of PowerShell Core scripts


 

Summary: Learn the basics about Docker, and see how to use it for PowerShell Core script testing on different operating systems.

I'm Dan Ward, a Boston-based .NET software engineer who is just plum crazy about PowerShell and automation. Earlier this year, I uploaded my first GitHub project—a PowerShell whitespace cleaner that also replaces aliases with commands and fixes casing issues. Like any manager of a new project, I was excited to see people using it and providing feedback and enhancement requests. But I became concerned when bug reports started coming in: "But I tested this on my machine before uploading, like, a thousand times!" (How often have you said that?)

PowerShell Core

Turns out I was testing in Windows PowerShell on a Windows machine, and the users having issues were using PowerShell Core that was running natively on non-Windows machines. Yes, the next generation of PowerShell is open source, and runs on Windows, Macs, and many Linux distributions. If you haven't read up on PowerShell Core yet, you should—or even better, download it and try it out now.

For most scripting activities, Windows PowerShell and PowerShell Core are very similar—but there are still some differences. And because PowerShell Core now runs across different operating systems, you need to start testing across these operating systems if you want to find any unexpected incompatibilities before your clients do.

But, like you, I really don't want to buy a lot of new hardware to test my work on different operating systems. And setting up VMs for all these operating systems is time consuming.

Docker to the Rescue!

Docker is an awesome containerization technology, and if somehow you haven't read about it you need to right now. One of the many benefits of using containers is that you can easily package up software to share with others. And the PowerShell Core team is doing just that: whenever they put out a new version, they provide packages for native installation and Docker images with PowerShell Core for popular operating systems. Want to test the latest version of PowerShell Core on Ubuntu? Just pull that image and start it up. In fact, let's do that right now.

Docker walkthrough with PowerShell Core images

Note: this walkthrough uses Docker for Windows on a Windows 10 Pro machine. If you have an older Windows operating system, you would need to use Docker Toolbox, which may not work out of the box in PowerShell.

If you want to follow along on your own machine, install Docker for Windows before continuing, and download the PowerShell whitespace cleaner.

First, in a browser, let's check out the Microsoft PowerShell Core Docker image repository.

Screenshot of image repository

There are several things to note here:

  • This page is served from Docker Hub. Docker Hub is a public registry; a place where anyone can create a repository and can upload or download container images.
  • This Docker Hub repository name is: microsoft/powershell. Repository names are [team or person name]/[project name].
  • A tag is a label that helps identify a unique image. In the PowerShell Core repository, you can see they are using tags to identify not just different operating systems, but versions of the operating systems.
  • When working with images, you identify them with [repository name]:[tag name]. However, :[tag name] is optional. If tag name is not specified, Docker uses the latest tag. In looking at the microsoft/powershell repository, latest looks like a reference to ubuntu16.04 (same size).

Great! So how do I know what images I have installed locally, and how do I pull an image to my machine?

Listing and pulling images

To list images on your local machine, use the docker images command.

PS C:\> docker images

REPOSITORY     TAG     IMAGE ID     CREATED     SIZE

PS C:\> # no images

Let's pull down a PowerShell Core image for a popular Linux distribution named Ubuntu; we'll use the 16.04 version.


PS C:\> docker pull microsoft/powershell:ubuntu16.04
ubuntu16.04: Pulling from microsoft/powershell
d5c6f90da05d: Pull complete
bbbe761fcb56: Pull complete
7afa5ede606f: Pull complete
f6b7253b56f4: Pull complete
2b8db33536d4: Pull complete
34ce21a172dd: Pull complete
db4e21036b1a: Pull complete
a553072824c6: Pull complete
cc09983fea92: Pull complete
3464f05d5785: Pull complete
Digest: sha256:c91708f5ba6f3b55da158eac0380f3c2bb907039d547b7ae52564dd7a0a3af8a
Status: Downloaded newer image for microsoft/powershell:ubuntu16.04
PS C:\> # let’s make sure we see that image
PS C:\> docker images
REPOSITORY           TAG         IMAGE ID     CREATED      SIZE
microsoft/powershell ubuntu16.04 bbbb17a9d348 11 hours ago 381MB
PS C:\>

As you can see, the first run of docker images had no results. After pulling microsoft/powershell:ubuntu16.04, we now have one image on the machine.

Creating, starting, and connecting to a container

Now that we have the image, we can create a container, and start it up in one step, with the docker run command.


PS C:\> # this is a normal Windows PowerShell
PS C:\> docker run -i -t microsoft/powershell:ubuntu16.04 powershell
PowerShell v6.0.0-beta.7
Copyright (C) Microsoft Corporation. All rights reserved.
PS /> # pretty sure I’m in PowerShell... but that PS /> prompt looks different

Hold on, how can I be sure I'm in a PowerShell session in a Linux container?


PS /> dir
Directory: /
Mode   LastWriteTime   Length Name
----   -------------   ------ ----
d----- 8/2/17 2:10 PM         bin
d----- 4/12/16 8:14 PM        boot
d----- 9/17/17 7:09 PM        dev
d----- 9/17/17 7:09 PM        etc
d----- 4/12/16 8:14 PM        home
d----- 9/14/17 5:16 AM        lib
d----- 8/2/17 2:09 PM         lib64
d----- 8/2/17 2:09 PM         media
d----- 8/2/17 2:09 PM         mnt
d----- 9/14/17 5:16 AM        opt
d-r--- 9/17/17 7:09 PM        proc
d----- 9/17/17 7:09 PM        root
d----- 8/2/17 2:09 PM         run
d----- 9/13/17 3:58 AM        sbin
d----- 8/2/17 2:09 PM         srv
d-r--- 9/17/17 7:09 PM        sys
d----- 9/17/17 7:09 PM        tmp
d----- 9/14/17 5:17 AM        usr
d----- 9/14/17 5:16 AM        var
PS /> $PSVersionTable
Name                       Value
----                       -----
PSVersion                  6.0.0-beta
PSEdition                  Core
GitCommitId                v6.0.0-beta.7
OS                         Linux 4.9.41-moby #1 SMP Wed Sep 6 00:05:16 UTC 2017
Platform                   Unix
PSCompatibleVersions       {1.0, 2.0, 3.0, 4.0...}
PSRemotingProtocolVersion  2.3
SerializationVersion       1.1.0.1
WSManStackVersion          3.0
PS /> exit
PS C:\> # back to a Windows prompt

I just typed 2 commands and I now have a new operating system, in a tiny container, running on my local machine! Just docker pull and docker run; that's it!

Here's exactly what happened:

  • docker pull downloaded an image, and tagged ubuntu16.04, from the microsoft/powershell repository.
  • docker run created a new container from that local image, started it, and ran the command powershell. This launched a PowerShell shell in the container. You can see the "PowerShell v6.0.0-beta.7" header.
  • In the container, dir displays a decidedly Unix—not Windows—folder listing. However, the format of the directory listing is the default PowerShell layout of content, with Mode, LastWriteTime, Length, and Name fields.
  • $PSVersionTable displays version information about PowerShell. Note that the platform is Unix.
  • Typing exit closed the connection to the container, stopped the container, and returned us to a normal Windows PowerShell command prompt.

(FYI, the docker run -i -t options create an interactive terminal with the container. Learn more at Docker's documentation.)

Listing containers

So we just created a container, but where is it? We display containers with the docker ps command.


PS C:\> docker ps
CONTAINER ID    IMAGE    STATUS    NAMES
PS C:\> # no containers?

Um...what? Ah, like the Unix ps command, the docker ps command by default shows only running containers. We need to run docker ps -a to show all containers.


PS C:\> docker ps -a
CONTAINER ID IMAGE                            STATUS                          NAMES
d05a481fcbf1 microsoft/powershell:ubuntu16.04 Exited (0) 20 seconds ago     blissful_kepler
PS C:\>

(Docker command output in the walkthrough has been edited for display purposes.)

In this simple container listing:

  • You can see the image named used to create the container, and that it's not running: Status = 'Exited'.
  • Container ID has a unique value that can be used to identify and manage the container.
  • Names also has a unique value, blissful_kepler (two random strings combined together), that can be used to identify and manage the container.

Creating a container with a specific name

If you create a container, but don't specify a name, Docker creates a random name for you. And while that random value is easier to work with than the Container ID value, it's not ideal if we are going to work with the container over and over. Let's create a new container named MyContainer from the same image, and start it up.


PS C:\> # specify --name to create container with a specific name
PS C:\> docker run -i -t --name MyContainer microsoft/powershell:ubuntu16.04 powershell
PowerShell v6.0.0-beta.7
Copyright (C) Microsoft Corporation. All rights reserved.
PS /> # we are back in a container; note the PS /> prompt
PS /> # but let’s just exit for now
PS /> exit
PS C:\>

So now let's take another quick look at the container list:


PS C:\> docker ps -a
CONTAINER ID IMAGE                            STATUS                    NAMES
ffa33fdae088 microsoft/powershell:ubuntu16.04 Exited (0) 10 seconds ago MyContainer
d05a481fcbf1 microsoft/powershell:ubuntu16.04 Exited (0) 5 minutes ago blissful_kepler

Now we have two containers, both created from image microsoft/powershell:ubuntu16.04.

Stopping and removing a container

At this point, let's get rid of container blissful_kepler. First, make sure it's stopped with docker stop, and then remove it with docker rm.


PS C:\> # if you run stop and it’s already stopped, that’s OK
PS C:\> docker stop blissful_kepler
blissful_kepler
PS C:\> docker rm blissful_kepler
blissful_kepler
PS C:\> docker ps -a
CONTAINER ID IMAGE                            STATUS                   NAMES
ffa33fdae088 microsoft/powershell:ubuntu16.04 Exited (0) 2 minutes ago MyContainer
PS C:\> # only MyContainer is left

Copying content into a container

So far, this container has been helpful if you want to test small, ad-hoc commands on another operating system. But if you really want to test any preexisting code, you need to copy that code into the container and run it within the container. So let's copy over the entire PowerShell Beautifier project. On my local machine, the Beautifier is located at C:\code\GitHub\PowerShell-Beautifier. We'll copy that folder into MyContainer, under the container's /tmp path. To do this, we use the docker cp command.


PS C:\> # running this in Windows PowerShell, not from within the container
PS C:\> docker cp C:\Code\GitHub\PowerShell-Beautifier MyContainer:/tmp

Starting and reconnecting to a container

Now reconnect to MyContainer to confirm the PowerShell-Beautifier folder content is there. When we first created MyContainer, we used docker run -i -t [image name] powershell to create it from the image and start it. Now that we have MyContainer, we just need to start it with docker start, and then reconnect to it with docker exec -i -t [container name] powershell. This creates an interactive terminal with a PowerShell shell.


PS C:\> # first we need to start the container
PS C:\> # if you run start and it’s already started, that’s OK
PS C:\> docker start MyContainer
MyContainer
PS C:\> # let’s make sure it’s running
PS C:\> docker ps -a
CONTAINER ID IMAGE                            STATUS        NAMES
ffa33fdae088 microsoft/powershell:ubuntu16.04 Up 11 seconds MyContainer
PS C:\> # because it’s running we don't need to add the -a to see it
PS C:\> docker ps
CONTAINER ID IMAGE                            STATUS        NAMES
ffa33fdae088 microsoft/powershell:ubuntu16.04 Up 20 seconds MyContainer
PS C:\> # now connect
PS C:\> docker exec -i -t MyContainer powershell
PowerShell v6.0.0-beta.7
Copyright (C) Microsoft Corporation. All rights reserved.
PS /> # we are back in the container; note the PS /> prompt

Confirming folder now exists in MyContainer

To confirm the content is there, change the directory to /tmp/PowerShell-Beautifier, and get a directory listing:


PS /> # we are inside MyContainer
PS /> cd /tmp/PowerShell-Beautifier
PS /tmp/PowerShell-Beautifier> dir
Directory: /tmp/PowerShell-Beautifier
Mode   LastWriteTime     Length   Name
----   -------------     ------   ----
d----- 9/11/17 11:14 PM           docs
d----- 9/11/17 10:11 PM           src
d----- 9/11/17 10:11 PM           test
------ 6/11/17 4:05 PM   1086     LICENSE
------ 9/9/17 9:54 PM    7638     README.md
PS /tmp/PowerShell-Beautifier> # nice!

Running a test script inside MyContainer

Now let's test a PowerShell script inside the container. The PowerShell Beautifier project comes with a test script /test/Invoke-DTWBeautifyScriptTests.ps1. This script runs a series of reformatting tests to make sure the Beautifier works correctly for different scenarios. Let's run this script in MyContainer.


PS /tmp/PowerShell-Beautifier> # we are inside MyContainer
PS /tmp/PowerShell-Beautifier> cd test
PS /tmp/PowerShell-Beautifier/test> ./Invoke-DTWBeautifyScriptTests.ps1
Importing beautifier module: DTW.PS.Beautifier
Processing folder: Case
File: Commands.ps1
File: Members.ps1
File: ParameterAttributes.ps1
File: Parameters.ps1
File: Types.ps1
Processing folder: CompleteFiles
File: Clear-PSFSitecoreRecycleBin.ps1
File: Remove-PSFOldContent.ps1
Processing folder: FileEncoding
File: ASCII_NoBOM.ps1
File: UTF16_BE_BOM.ps1
File: UTF16_BE_NoBOM.ps1
File: UTF16_LE_BOM.ps1
File: UTF16_LE_NoBOM.ps1
File: UTF8_BOM.ps1
File: UTF8_NoBOM.ps1
Processing folder: Rename
File: Alias.ps1
Processing folder: Whitespace
File: CmdletDefinition.ps1
File: DotSource.ps1
File: NoNewLineAtEndOfFile.ps1
File: NonWindowsLineEnding.ps1
File: WithinLine.ps1
File: Indentation.ps1
File: Indentation.ps1
File: Indentation.ps1
All tests passed - woo-hoo!
PS /tmp/PowerShell-Beautifier/test>

Invoke-DTWBeautifyScriptTests.ps1 also has a -Quiet option. If the test script runs without any errors, then only Boolean $true is returned at the end. This is helpful for automating the test script described in the next section.


PS /tmp/PowerShell-Beautifier/test> # we are inside MyContainer
PS /tmp/PowerShell-Beautifier/test> # run the script sans output
PS /tmp/PowerShell-Beautifier/test> ./Invoke-DTWBeautifyScriptTests.ps1 -Quiet
True
PS /tmp/PowerShell-Beautifier/test> exit

Automating the testing process

Running all these Docker commands manually is a good way to learn the Docker command line interface. It's also a good way to get inside a PowerShell Core instance for another operating system, to try some small commands. But if you want to use these Docker images to automate testing of your existing PowerShell scripts, you would need to automate all the steps above. And you would want to be able to:

  • Specify one or more source paths to copy into a container.
  • Specify which test script to run in the container.
  • Specify which Docker images to test against, and validate they exist.
  • Create missing containers for the images we specify, start containers when we need to, and stop them when we're done.
  • Maybe even allow the override of the default Docker Hub project with something other than microsoft/powershell.

There's actually a script ready for you right now that does all this! Its name is Invoke-RunTestScriptInDockerCoreContainers.ps1, and it's in the PowerShell Beautifier project under /test/Automation. Check out the readme, or go straight to the source yourself. Feel free to copy the script, or clone and fork the entire project if you'd like to try out the PowerShell Beautifier yourself.

First, let's pull another image from Docker Hub. Let's get the centos7 image.


PS C:\> docker pull microsoft/powershell:centos7
centos7: Pulling from microsoft/powershell
2d490773b5db: Pull complete
8cd0d9ccbcbc: Pull complete
f54c59906966: Pull complete
963d4e7132af: Pull complete
98f59503f09a: Pull complete
Digest: sha256:a6fdb2a29195f3280cb4df171f8fbabc20d0e6fbee8bb4b2fd9de66e1f16c33e
Status: Downloaded newer image for microsoft/powershell:centos7
PS C:\>

Now let's run Invoke-RunTestScriptInDockerCoreContainers.ps1. By default, if you copy the entire Beautifier project, you may not need to specify any values for the parameters. The default values for the script parameters should cover everything. The only thing you might want to specify is -TestImageNames, which currently defaults to ubuntu16.04, centos7, and opensuse42.1. But just to show you the parameters in action, we'll specify -SourcePaths, -TestFileAndParams and -TestImageNames.

Just to be clear: currently on my machine I have images for ubuntu16.04 and centos7, but not opensuse42.1.


PS C:\> # running this in Windows PowerShell, not from within the container
PS C:\> C:\code\GitHub\PowerShell-Beautifier\test\Automation\Invoke-RunTestScriptInDockerCoreContainers.ps1 `
>> -SourcePaths C:\Code\GitHub\PowerShell-Beautifier `
>> -TestFileAndParams 'PowerShell-Beautifier/test/Invoke-DTWBeautifyScriptTests.ps1 -Quiet' `
>> -TestImageNames @('ubuntu16.04','centos7','opensuse42.1')
Testing with these values:
Test file: PowerShell-Beautifier/test/Invoke-DTWBeautifyScriptTests.ps1 -Quiet
Docker hub repo: microsoft/powershell
Images names: ubuntu16.04 centos7 opensuse42.1
Source paths: C:\Code\GitHub\PowerShell-Beautifier
Image opensuse42.1 is not installed locally but exists in repository microsoft/powershell
To download and install type:
docker pull microsoft/powershell:opensuse42.1
Testing on these containers: ubuntu16.04 centos7
ubuntu16.04
Preexisting container found
Starting container
Getting temp folder path in container
Copying source content to container temp folder /tmp/
C:\Code\GitHub\PowerShell-Beautifier
Running test script on container
Test script completed successfully
Stopping container
centos7
Preexisting container not found; creating and starting
Getting temp folder path in container
Copying source content to container temp folder /tmp/
C:\Code\GitHub\PowerShell-Beautifier
Running test script on container
Test script completed successfully
Stopping container
PS C:\> # the test script ran correctly in all containers! woo-hoo!

Conclusion

As you can see, Docker is a simple and powerful technology for packaging and distributing software. While this article focused on just using the PowerShell Core images, you should check out the Docker Hub to see what other images exist. You can also learn more about how to incorporate Docker into your development processes.

Also, feel free to submit a ticket if you have an issue or enhancement request for the test script or the PowerShell Beautifier. And feel free to contact me.

Thank you for your time and interest!

Dan Ward, guest blogger

 

Comments (0)

Skip to main content