Simple Talk is now part of the Redgate Community hub - find out why

Practical PowerShell Unit-Testing: Mock Objects

Pester allows you to automate the testing of PowerShell scripts. It can test the output of a function as you develop it by validating simple data (scalars) and arrays, Pester allows you to focus on the one function you want to test by using 'mocking' to fake all the other functions and Cmdlets, It also uses Parameterized tests save you from writing essentially the same test over and over with just a different input value

Contents

In the previous installment, you saw how to install and use Pester, and the basic structure of unit tests in PowerShell. This article covers mocking and parameterized test cases. The third installment will conclude with data and program flow validation.

Mocking

Mock objects, or ‘mocks’, are objects that are deliberately created to simulate other objects as simply as possible.  They  are frequently used in unit-testing to mimic the behavior of real objects in controlled ways, when it is impractical or time-consuming to use the real object.  Pester creates mocks with the Mock command, which has a very simple syntax yet provides a lot of flexibility at the same time. Let’s see how it works.

Say you want to create a simple function that returns a list of file names in the current directory that are text files (i.e. those with a .txt suffix). The first test you might write is to see if it can return a single such file:

You might write the code for Get-TextFileNames like this:

That is, use Get-ChildItem to list the files in the current directory, filter the list to those ending in .txt, and from the remaining items-a list of FileInfo objects-return just the name.

Run that test and it will fail (unless you happen to have a file called a923e023.txt-and no other text files-in your directory. With no text files in your directory, you would see this:

You can immediately see that it is a fragile test indeed that happens to depend on the contents of your current directory. Even if you create that text file here for the sake of the test, the next time you want to run your test you might be in a different directory. Or perhaps a colleague wants to run the test on his/her machine. Or you want your build machine that supports your continuous integration to run it. The goal, then, is to isolate tests from outside influences-like your file system. This is a great use case for mocks.

For this test, then, we want to mock the Get-ChildItem cmdlet so that it returns a single FileInfo object representing a file named a923e023.txt. To do this we use the Mock command, indicating the command we want to mock and what we want it to return rather than calling the real Get-ChildItem:

The MockWith argument is a PowerShell script block, i.e. just a set of statements surrounded by braces. In this case, we have a single statement:

This is a concise way to construct a PowerShell object that looks sufficiently like a particular real object for our needs. In this case, we are creating an object with a single Name property, because that is precisely the property that our code is using; here it is again:

2084-img11F.jpg

Now let’s put that mock into the test shown earlier (here I have abbreviated the command by omitting the parameter names -CommandName and -MockWith).

This test will now pass no matter what directory you are in or what machine you are on. We have completely isolated the test from the real Get-ChildItem and thus from the real file system.

This test might seem somewhat pointless, though, because I fed it the precise input I am attempting to validate against. But that is not the case! This is a real test and a useful one. It validates that the current implementation of Get-TextFileNames is working (for this one set of data). Here are just a couple variations of the implementation where that test would have failed:

2084-img120.jpg

By controlling for Get-ChildItem (with a mock) we are allowing the test to validate the remaining cmdlets within Get-TextFileNames. Let’s really give Get-TextFileNames a workout by expanding our test coverage a bit to see how robust it is. Using the same simple mock, returning different sets of data, we can mimic a real directory that could have varied contents. I have included the test above as the first test here, but expanded its description to make it clear what that test is validating.

There are several more tests I would include, even for a function as simple as this, but I have kept the list short for this article. The above is a perfectly valid set of tests you might use, but in practice, I would use the power of PowerShell to streamline that a bit. First, introduce a helper function, a mini-factory, to create my data set. Here I am taking in a list of names and generating FileInfo-like objects, including both the Name property that you have seen in the above tests, and the FullName property, which you might use in additional tests.

With that helper function added to the test file, the above set of tests can now be written more concisely as:

Using the CreateFileList mini-factory function, we reduced the suite of unit tests by about one-third. After introducing a few more Pester concepts, you will see a further substantial reduction for this same group of tests.

Mocking gets particularly interesting when you also add the ParameterFilter and/or use multiple mocks, discussed shortly.

But first, I want to mention the potential for abuse with mocks. This is not peculiar to PowerShell, but it certainly applies to PowerShell now that you have mocking capability with Pester. The issue is that of state-based testing (testing the result of a method) vs. behavior-based testing (testing what happened during a method call). While there are arguments for both sides of the issue, I believe that judicious use of both techniques provides the best results. Often the code itself tells you what kind of test might be a better fit: if you have a method that generates no side effects and returns a value, then clearly state-based testing is better. For a method that has side effects and returns no result, then clearly behavior-based testing is appropriate. Kenneth Cochran, in this Stack Overflow question, elaborates nicely on this point. The problems with behavior-based testing are nicely summarized by Matt Wrock in his post Unit Testing PowerShell and Hello Pester:

“We are reaching deeper into the function and assuming we know more that we should about what the function is doing. We are testing the implementation details of the function rather that the outcome of its state. The behavior based tests open the testing framework up to added fragility since changes in implementation often requires changing the tests. Mocking can lead developers to overusing behavior based tests since it makes it so easy to test the function’s interactions at various points of the function’s logic.”

PowerShell, as Wrock further points out, is perhaps more challenging to do state-based testing just due to the nature of the “PowerShell ecosystem” (Wrock’s phrase, which I think is particularly apropos). With my own explorations, thus far, I aim for state-based testing, but I still find I need to use behavior-based testing in many cases.

Multiple Mocks Example

Here I am setting up two mocks for the same cmdlet, Select-String. The first mock introduces the ParameterFilter argument that lets you limit the set of inputs for which a mock will trigger. Here it is rather selective; it will be triggered only for values of $Path sent to Select-String that match the filter (which is a regular expression specifying to match anything ending in B, D, or E).

There are two key things to know when using multiple mocks. First, mocks are examined for triggering in the order you define them. Second, only one mock gets triggered for a given CommandName value. Therefore, because the unconditional mock occurs after the more selective mock, the second mock is a “catch-all”; anything that the first mock chose to ignore will trigger the second mock.

Thus, the first mock will trigger for “fileB”, returning “matched!”, while for an input of “fileA” the second mock will trigger, returning nothing (because the MockWith parameter is not specified). Extend that with more cases and you have successfully emulated a standard switch statement.

Logic-Added Mock Example

The logic in this next example is just another way to produce results identical to the previous example. Here, instead of using multiple mocks and a ParameterFilter, though, it uses additional logic within a single mock script block.

The MockWith parameter is just a standard PowerShell script block, so you may include whatever logic you wish. And here, you could even use a standard switch statement, if you had more cases to handle.

Now let’s put it all together. Here is a function under test (GetFilesLackingItem) and a single unit test for it. The function returns the names of files that do not contain a line having the specified pattern. It uses the standard PowerShell cmdlets Get-ChildItem and Select-String to do most of the work, then decorates them with some simple manipulations. It is those manipulations that need to be tested rather than the functionality of either cmdlet. Thus, the test sets up mocks for both Select-String and Get-ChildItem. You will see the data generation technique described just above used here to provide mock data for Get-ChildItem. You will also see the multiple-mock technique described above to provide intelligence to the mock for Select-String. (You can try out this code by copying both source and test into a single Demo.Tests.ps1 file and then running Invoke-Pester on the containing directory.)

2084-pencil.jpg

Use one or more mocks to isolate your unit under test.

Parameterized Test Cases

A parameterized test case is a simple but powerful testing technique, but surprisingly unknown enough to not even have spawned a Wikipedia entry! It is really as straightforward as it sounds: a test case that takes parameters. It is analogous to a function. Consider first this function without parameters:

When you invoke f() you always get the sum of 5 and 23. Quite… unexciting. Now jazz that up with parameters creating-you guessed it-a parameterized function:

Now you can still get 28 by invoking f(5, 23) but having parameterized your inputs you now have the freedom to  do f(0, -123), f(42, 42), and so on ad infinitum.

A parameterized test serves exactly the same purpose. Say you write a regular test case that validates some function f with 1 feinberger returns 2 ounces of quadrotriticale. Then you write another test that validates f with 2 feinbergers returns 3 ounces of quadrotriticale. This second test is virtually identical to the first, except for the size of the input and the resultant size of the output. To eliminate all the duplicated code, you just have to feed sets of input/output parameters to a parameterized version of the test function.

You can find support for parameterized test cases in NUnit for .NET but Pester does not have native support for this… yet! I submitted a feature request for this recently and the Pester crew responded readily to adding native support in the near future. But you do not have to wait; due to the nature of PowerShell it is elegantly simple to do this yourself.

Simple Parameterized Test Example

First, wrap your test inside a function. The function signature should include all the input and output elements you wish to parameterize and, of course, the body of the function should use those parameters (the Test-SafeProperties function in the code below). Next specify each set of inputs and outputs to feed to the function as an array whose elements are ordered to match the parameter order of the function signature. Add as many data sets (tests) to this list as you wish (the $dataSets variable in the code). Finally, marry the two together by piping the data to the function applying PowerShell’s splatting functionality (see about_splatting).

Parameterized Test Example with ContextUsing

The example above actually provides a complete, viable technique to do parameterized tests. However, I try to aspire to the three great virtues of a programmer (as espoused by Larry Wall, the author of Perl); in this case, laziness is particularly apropos. To be able to write a more concise version of the above code that performs equivalently, I wrote an extension for Pester’s Context command called ContextUsing. The original syntax of the Context command is:

The new  ContextUsing command adds the TestCases parameter:

The code below shows one example usage. Notice that the script block must start with a param() statement that enumerates the parameters of the script block (or anonymous function if you prefer). Each test case, then, must specify values for each of those parameters.

2084-pencil.jpg

Use a parameterized test to avoid massive code duplication.

 Note 1: Just as the Test Anatomy section in part 1 warned you about placement of opening braces for script blocks, the opening parenthesis of the test case list here must start on the same line as the ContextUsing as shown, and the closing parenthesis must be on the same line as the opening brace of the script block.

Note 2: A handy technique is to add a parameter ($description in the above code) that rather than being used by the test for validation just describes the test. That parameter is used in the description of the It command, and is displayed in your test output.

Note 3: To add ContextUsing to your Pester distribution, simply copy the ContextUsing.ps1 file (attached to this article) into your Pester distribution, then re-import the Pester module into your PowerShell window (i.e. use Import-Module with the -Force parameter).

Note 4: ContextUsing does not support passing only a single test case. That is, there must be at least two sets of data supplied to the TestCases parameter. This is not really a limitation per se, because if you only have one test case, you do not need ContextUsing!

Get-TextFileNames Example Revisited

As promised earlier, let’s go back to the Get-TextFileNames example at the top of this article and see how, with parameterized tests you can achieve yet another dramatic reduction in line count. If you look back at the 4 tests for Get-TextFileNames, you will observe that all four tests are exactly the same except for the list of files being fed into the mock and the result that we expect to get back from the function under test. In this case, it is only a couple lines of code but you can imagine that more involved tests with perhaps a dozen or two dozen lines would cause a tremendous amount of duplicated code. Parameterized tests, as you have seen, eliminate this code duplication. Without further ado, here is the set of 4 test for Get-TextFileNames rewritten as a set of parameterized tests.  I have spread out and commented the test data to make it easier to visually distinguish the arguments, but there are now only two lines of actionable code: one to define the mock and one to validate the function with the configured mock.

Note that only two of the three parameters are used in running the test: $files and $expectedResult. The third parameter ($description), as mentioned above, is for identifying the test. Run the tests and you will see that the description is valuable to have:

Conclusion

This article showed how to make one test function actually cover multiple test cases; PowerShell makes that particularly easy to do. It also introduced mocks, which, as you have seen, make it easy to isolate components so you can focus on testing just your chunk of code without external influences. But mocks serve an additional purpose, providing hooks to validate program flow. The next installment explains how to do that, as well as validating data, providing some additional support to validate array data.

How you log in to Simple Talk has changed

We now use Redgate ID (RGID). If you already have an RGID, we’ll try to match it to your account. If not, we’ll create one for you and connect it.

This won’t sign you up to anything or add you to any mailing lists. You can see our full privacy policy here.

Continue

Simple Talk now uses Redgate ID

If you already have a Redgate ID (RGID), sign in using your existing RGID credentials. If not, you can create one on the next screen.

This won’t sign you up to anything or add you to any mailing lists. You can see our full privacy policy here.

Continue