Why isn’t Test Driven Development more widely adopted and accepted by the PowerShell community?

We’ve all heard that TDD (Test Driven Development) means that you write unit tests before writing any code. Most of us are probably writing functional or acceptance tests after the fact because the idea of Test Driven Development isn’t clearly defined, at least not in my opinion. I originally thought it meant to write thorough unit tests to test all functionality for a specific piece of code such as a PowerShell function from start to finish before writing any of the production code for the function itself. This would be very difficult to accomplish in the real world and in my opinion, it’s one of the reasons Test Driven Development isn’t more widely adopted.

The idea of writing all of the tests first is a false premise and maybe a misconception on my part as well as others. The unit tests are actually written in parallel with the production code. A simple failing test is written first and then just enough production code is written to make that one simple test pass. Then another simple failing test is written and then the production code to make it pass is written and so on. The goal is to write each one in very small, quick, and simple iterations otherwise TDD adds unnecessary overhead and won’t be adopted.

My workflow for TDD is shown below. I created this myself (it’s NOT some image copied from the Internet).


My setup for writing PowerShell code when using Test Driven Development is to open two tabs in the PowerShell ISE or SAPIEN PowerShell Studio. One of the tabs is for my tests and the other is for a PowerShell function that I’m developing. I also have the PowerShell console open on a separate monitor and I run the following function in the console to assist me in keeping on task as far as my TDD workflow goes:

The most recent version of the Invoke-MrTDDWorkflow function shown in the previous code example can be found in my PowerShell repository on GitHub.

I’ll start out with an empty folder:


The Invoke-MrTDDWorkflow function is run, it asks the question “Is the code complete?” since there are currently no failing tests. The default button is “No” so the <enter> key can be pressed instead of having to reach for the mouse which would take more time.


After selecting “No“, the following message is displayed:


This is where most who are new to TDD make the mistake of thinking they have to write a complete suite of tests to thoroughly test every aspect of the code they’re going to write. Some may disagree with me but that methodology is incorrect and it’s what drives people away from using TDD. Also keep in mind that you don’t necessarily know how the task is going to be accomplished at this point so try to write tests generically enough that the result could be achieved different ways since there are so many different ways to accomplish the same task in PowerShell. This will help prevent having to rewrite tests when code is refactored in the future.

I’m going to write a PowerShell function that doesn’t exist yet. With TDD, you’re not allowed to write any production code until a test is written for it first. You’re also not allowed to write more of a test than is sufficient to fail. Those are the first two laws of TDD. I’ll simply write a test to see if the function exists.

Depending on whether or not -ErrorAction SilentlyContinue is included, the test returns more or less output. I prefer including it to minimize the output since I simply want to know the result of the test without all of the details. Lots of tests will be added and run by the time the function is completed and more details per test means more time to determine if each individual test passed or not.


Now that I have one simple failing test, I need to write code for the Get-MrSystemInfo function until that one test passes. This is the third law of TDD. You are not allowed to write more production code than is required to make the current test pass. I press <enter> in my console session so my workflow goes to the next step and by default it will run the tests every 30 seconds and prompt me with the following message until all tests pass:


As mentioned before, I’ve only written the code required to make the current test pass. No more, no less:

The test passes and I’m once again asked: “Is the code complete?” since there are currently no failing tests:


I answer no again and repeat the same process of writing another test:


The function needs to return the system manufacturer so I add that one single test:

After saving the new test in the Get-MrSystemInfo.Tests.ps1 file, I press enter again and a different test is specified in my workflow:


I write only the code that makes this new test pass:

The test passes and I’m asked again if the code is complete. I’m not so I’m back in the same workflow loop as before, writing another failing test. One thing to keep in mind is each iteration of writing a test and then production code to make the test pass should be quick and easy.

Writing the production code for the function to make this new test pass requires some of the existing code be refactored:

When new code or the refactoring of existing code breaks existing functionality, I can immediately see it as in this scenario where the second test that previously passed is now failing for some reason:


The problem is “Manufacturer” was misspelled in the previous code example when it was refactored. This problem has been corrected and now all tests pass.

You might be thinking the hard drive capacity or size won’t do much good without the drive letter and you’re right so that would be the next test to write.

Keep repeating this process until the function is complete. The production code can be refactored as much as you want even months down the road, even when your organization has experienced turnover and the person or people who originally wrote the code are no longer with your organization. Simply rerun the unit tests and you’ll know if the code still produces the desired results. Best of all, you’ll no longer have to worry about breaking production code when adding new features or refactoring existing code in the future.



  1. Paul Cassidy

    The first test where you validate the command exists fails for me no matter if the function is there of not.

    • Mike F Robbins

      If you remove the -ErrorAction SlientlyContinue part, what does the error message say when it fails? I’ve tested with both Pester version 3.3.5 and 3.4.0.


      Normally you would have two files in my scenario, one named Get-MrSystemInfo.ps1 that contains the function and one named Get-MrSystemInfo.Tests.ps1 that contains all of the tests. The first three lines of the test works a bit of magic and dot-sources the function when Invoke-Pester is run:

      $here = Split-Path -Parent $MyInvocation.MyCommand.Path
      $sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path).Replace('.Tests.', '.')
      . "$here\$sut"
  2. fromthewoods (@fromthewoods5)

    Great write up Mike! I will be forwarding this to colleagues.

  3. csandfeld

    Really nice post Mike, very inspiring.

    To other readers, be aware that the BeOfType assertion used in the example to test that DiskSize is returned, was introduced in Pester 3.3.9


Leave a Reply

%d bloggers like this: