Skip to content

TDD

PowerShell TDD with Pester – Adding More Rules (part 2 of 3)

In this article, we’ll add more rules to our version number generator to handle things like bug fixes to major versions, pre-release labels and build labels.

To help guide the rules being implemented, below is a table of the incrementation rules that will be applied.

Element Incrementation Notes
Major Version in file used with major versions treated separately for incrementation purposes
Minor Latest value used within major version.
Patch One higher than previous version unless major or minor version has increased in which case should be 0
Pre-Release Label Removing it or leaving one in place won’t cause an increase (if no existing version exists on that level) but adding it will increase the patch version the label changing doesn’t matter
Build Label Does not cause any incrementations.

Seventh Test – Minor Version Increase Resets Patch

This test is about minor version number changes causing the patch to reset to 0 when the minor version number changes but otherwise it should be increased.

Failing Test

It 'Minor version change in file version (<fileVersion>) compared to published version (<publishedVersion>) causes patch to reset to 0 (<expectedVersion>)' -ForEach @(
  @{ FileVersion = "1.0.0"; PublishedVersion = "1.0.0"; ExpectedVersion = "1.0.1" }
  @{ FileVersion = "1.1.0"; PublishedVersion = "1.0.0"; ExpectedVersion = "1.1.0" }
  @{ FileVersion = "1.0.0"; PublishedVersion = "1.1.0"; ExpectedVersion = "1.1.1" }
  @{ FileVersion = "1.1.0"; PublishedVersion = "1.1.0"; ExpectedVersion = "1.1.1" }
) {
  # Arrange
  $fileVersion = [SemanticVersion]$fileVersion
  $publishedVersion = [SemanticVersion]$nugetVersion
  $preReleaseLabel = ""
  $buildLabel = ""

  $expectedVersion = [SemanticVersion]$expectedVersion

  # Act
  $newVersion = [SemanticVersion](Get-NextVersion -FileVersion $fileVersion -PublishedVersion $publishedVersion -PreReleaseLabel $preReleaseLabel -BuildLabel $buildLabel)

  # Assert
  $newVersion | Should -Be $expectedVersion
}

Making the Test Green

To make this test work, a few small changes are needed. A resetPatch boolean variable is needed, defaulting to false and the published version check needs splitting and a new method adding. Finally, the new patch version calculator needs some extra logic to handle the resetPatch variable.

Once this is all put together, it looks like this:

function Get-NextVersion {
  param (
    [SemanticVersion] $FileVersion,
    [SemanticVersion] $PublishedVersion
  )

  $newVersion = [SemanticVersion]"0.0.0"
  $notSet = [SemanticVersion]"0.0.0"
  $resetPatch = $false

  if ($FileVersion -ne $notSet -and $FileVersion -gt $newVersion) {
    $newVersion = $FileVersion
  }
  if ($PublishedVersion -ne $notSet) {
    if ($newVersion.Minor -gt $PublishedVersion.Minor) {
      $resetPatch = $true
    }
    if ($PublishedVersion -gt $newVersion) {
      $newVersion = $PublishedVersion
    }    
  }

  $patch = $newVersion.Patch + 1;
  if ($resetPatch) {
    $patch = 0
  }
  $newVersion = [SemanticVersion](Get-UpdatedSemanticVersion $newVersion -PatchVersion $patch)

  Write-Output $newVersion
}

This change will, however, cause our previous test to fail. To resolve this, update the second data set’s expected value from 1.1.1 to 1.1.0.

Eighth Test – Pre-release Label Increments Patch If Added But Not If Removed Or Already Present

This test builds the logic for handling the use of pre-release tags (e.g. “-dev” or “-test”). It should only increase the patch version if adding a label. The value of the label isn’t important.

Failing Test

It 'Pre-release tag increments patch if added (<preReleaseTag>) but not if removed or already present (<publishedVersion> --> <expectedVersion>)' -ForEach @(
  @{ FileVersion = "1.0.0"; PublishedVersion = "1.0.0"; PreReleaseLabel = "dev"; ExpectedVersion = "1.0.1-dev" }
  @{ FileVersion = "1.0.0"; PublishedVersion = "1.0.0-dev"; PreReleaseLabel = "dev"; ExpectedVersion = "1.0.0-dev" }
  @{ FileVersion = "1.0.0"; PublishedVersion = "1.0.0-dev"; PreReleaseLabel = ""; ExpectedVersion = "1.0.0" }
  @{ FileVersion = "1.0.0"; PublishedVersion = "1.0.0-dev"; PreReleaseLabel = "test"; ExpectedVersion = "1.0.0-test" }
  @{ FileVersion = "1.1.0"; PublishedVersion = "1.0.1-dev"; PreReleaseLabel = "dev"; ExpectedVersion = "1.1.0-dev" }
  @{ FileVersion = "1.1.0"; PublishedVersion = "1.0.1-dev"; PreReleaseLabel = ""; ExpectedVersion = "1.1.0" }
) {
  # Arrange
  $fileVersion = [SemanticVersion]$fileVersion
  $publishedVersion = [SemanticVersion]$publishedVersion
  $buildLabel = ""

  $expectedVersion = [SemanticVersion]$expectedVersion

  # Act
  $newVersion = [SemanticVersion](Get-NextVersion -FileVersion $fileVersion -PublishedVersion $publishedVersion -PreReleaseLabel $preReleaseLabel -BuildLabel $buildLabel)

  # Assert
  $newVersion | Should -Be $expectedVersion
}

Making the Test Green

The main code changes required are to do with determining if the patch version should be incremented or not. This is largely centred around whether or not the pre-release label is on the existing published package or not.

function Get-NextVersion {
  param (
    [SemanticVersion] $FileVersion,
    [SemanticVersion] $PublishedVersion,
    [string] $PreReleaseLabel
  )

  $newVersion = [SemanticVersion]"0.0.0"
  $notSet = [SemanticVersion]"0.0.0"
  $incrementPatch = $true
  $resetPatch = $false

  if ($FileVersion -ne $notSet -and $FileVersion -gt $newVersion) {
    $newVersion = $FileVersion
  }
  if ($PublishedVersion -ne $notSet) {
    if (![string]::IsNullOrWhiteSpace($PublishedVersion.PreReleaseLabel)) {
      $incrementPatch = $false
    }
    if ($newVersion.Minor -gt $PublishedVersion.Minor) {
      $resetPatch = $true
    }
    if ($PublishedVersion -gt $newVersion) {
      $newVersion = $PublishedVersion
    }    
  }

  $patch = $newVersion.Patch
  if ($resetPatch) {
    $patch = 0
  } elseif ($incrementPatch) {
    $patch++
  }
  $newVersion = [SemanticVersion](Get-UpdatedSemanticVersion $newVersion -PatchVersion $patch -PreReleaseLabel $PreReleaseLabel)

  Write-Output $newVersion
}

Ninth Test – Any Build Label Stops Patch Incrementation

If a build label is specified, it will stop patch incrementation from happening. It won’t stop resets though.

Failing Test

It 'Any build label (<buildLabel>) stops patch incrementation (<publishedVersion> --> <expectedVersion>)' -ForEach @(
  @{ FileVersion = "1.0.0"; PublishedVersion = "1.0.0-dev"; PreReleaseLabel = "dev"; BuildLabel = "1234"; ExpectedVersion = "1.0.0-dev+1234" }
  @{ FileVersion = "1.0.0"; PublishedVersion = "1.0.0-dev+1234"; PreReleaseLabel = "dev"; BuildLabel = "1234"; ExpectedVersion = "1.0.0-dev+1234" }
  @{ FileVersion = "1.0.0"; PublishedVersion = "1.0.0-dev+1233"; PreReleaseLabel = "dev"; BuildLabel = "1234"; ExpectedVersion = "1.0.0-dev+1234" }
  @{ FileVersion = "1.0.0"; PublishedVersion = "1.0.0"; PreReleaseLabel = ""; BuildLabel = "1234"; ExpectedVersion = "1.0.0+1234" }
  @{ FileVersion = "1.0.0"; PublishedVersion = "1.0.0"; PreReleaseLabel = "dev"; BuildLabel = "1234"; ExpectedVersion = "1.0.1-dev+1234" }
) {
  # Arrange
  $fileVersion = [SemanticVersion]$fileVersion
  $publishedVersion = [SemanticVersion]$publishedVersion

  $expectedVersion = [SemanticVersion]$expectedVersion

  # Act
  $newVersion = [SemanticVersion](Get-NextVersion -FileVersion $fileVersion -PublishedVersion $publishedVersion -PreReleaseLabel $preReleaseLabel -BuildLabel $buildLabel)

  # Assert
  $newVersion | Should -Be $expectedVersion
}

Making the Test Green

To make this test, the build number parameter needs adding and an “else if” condition adding after the pre-release label check to stop incrementing if a new tag isn’t present but a build number is.

function Get-NextVersion {
  param (
    [SemanticVersion] $FileVersion,
    [SemanticVersion] $PublishedVersion,
    [string] $PreReleaseLabel,
    [string] $BuildLabel
  )

  $newVersion = [SemanticVersion]"0.0.0"
  $notSet = [SemanticVersion]"0.0.0"
  $incrementPatch = $true
  $resetPatch = $false

  if ($FileVersion -ne $notSet -and $FileVersion -gt $newVersion) {
    $newVersion = $FileVersion
  }

  if ($PublishedVersion -ne $notSet) {
    if (![string]::IsNullOrWhiteSpace($PublishedVersion.PreReleaseLabel)) {
      $incrementPatch = $false
    } elseif ([string]::IsNullOrWhiteSpace($PreReleaseLabel) -and ![string]::IsNullOrWhiteSpace($BuildLabel)) {
      $incrementPatch = $false
    }
    if ($newVersion.Minor -gt $PublishedVersion.Minor) {
      $resetPatch = $true
    }
    if ($PublishedVersion -gt $newVersion) {
      $newVersion = $PublishedVersion
    }    
  }

  $patch = $newVersion.Patch
  if ($resetPatch) {
    $patch = 0
  } elseif ($incrementPatch) {
    $patch++
  }
  $newVersion = [SemanticVersion](Get-UpdatedSemanticVersion $newVersion -PatchVersion $patch -PreReleaseLabel $PreReleaseLabel -BuildLabel $BuildLabel)

  Write-Output $newVersion
}

Tenth Test – Reading Files

The next test, covered in the next blog, will cover testing the process of reading files.

PowerShell TDD with Pester – Adding More Rules (part 3 of 3)

PowerShell TDD with Pester – Adding More Rules (part 3 of 3)

This final article will cover reading mocked files and retrieving the version number from the CSProj or Chart.yaml file.

Tenth Test – Reading From Chart.yaml File

The first test is to read a Helm’s Chart.yaml file, extract the current current chart version and call the Get-NextVersion function.

Failing Test

Describe 'Read-CurrentVersionNumber' {
  It 'Extract current chart version from Chart.yaml and call Get-NextVersion' {
    # Arrange
    $expected = [SemanticVersion]"1.0.0"

    # Act
    $actual = [SemanticVersion](Read-CurrentVersionNumber -File "Chart.yaml)

    # Assert
    $actual | Should -Be $expected
  }
}

To give a fail (rather than non-running) test, add the following function:

function Read-CurrentVersionNumber {

}

Making the Test Green

As we’re reading a YAML file, we’ll use the powershell-yaml module.

To make the test pass, simple return “1.0.0” from the function:

function Read-CurrentVersionNumber {
  Write-Output "1.0.0"
}

Refactor to Read (Faked) File

To make the code (simulate) reading a file, we need to add a parameter to pass in the file name, process the YAML and return the version.

function Read-CurrentVersionNumber {
  param (
    [Parameter(Mandatory=$true)] [string] $file
  )

  Import-Module powershell-yaml

  $chartYaml = (Get-Content -Path $file | ConvertFrom-Yaml)

  Write-Output $chartYaml.version
}

We’ll also add some code to BeforeAll to mock certain functions to prevent module installs happening as part of unit tests.

  function Get-Content {
    Write-Output "
    apiVersion: v2
    appVersion: 1.0.0
    description: A Helm chart to deploy applications to a Kubernetes cluster using standard settings
    name: Generalised
    type: application
    version: 1.0.0    
    "

Eleventh Test – Reading From CSProj File

This will cover reading from the XML-formatted CSProj file.

  It 'Extract current chart version from a CSProj file' {
    # Arrange
    $expected = [SemanticVersion]"1.2.0"

    # Act
    $actual = [SemanticVersion](Read-CurrentVersionNumber -File "My.Project.csproj")

    # Assert
    $actual | Should -Be $expected
  }

Making the Test Green

We’ll put some logic around the code reading the version so that it handles YAML files vs XML files. We’ll also need to add another mock to handle the reading of the CSProj/XML file.

The code changes involve a new if statement that analyses the file name being passed:

function Read-CurrentVersionNumber {
  param (
    [Parameter(Mandatory=$true)] [string] $file
  )

  $fileName = Split-Path $file -Leaf

  if ($fileName -eq "Chart.yaml") {
    Import-Module powershell-yaml

    $chartYaml = (Get-Content -Path $file | ConvertFrom-Yaml)

    Write-Output $chartYaml.version
  } elseif ($fileName.EndsWith(".csproj")) {
    $projectVersion = $(Select-Xml -Path "$file" -XPath '/Project/PropertyGroup/Version').Node.InnerText

    Write-Output $projectVersion
  }
}

To work, this needs to following stub adding:

  function Select-Xml {
    Write-Output @{
      Node = @{
        InnerText = "1.2.0"
      }
    }
  }

PowerShell TDD with Pester – Setup and Basic Tests (part 1 of 3)

Following on from my experiment with TDD using C#, I wanted to look at TDD with PowerShell as that’s another language I currently use quite a bit. To do this, I will be using the Pester framework. These articles are covering how I built a function to generate a new version number for a NuGet package, Helm chart, etc… based upon the version number in a file (e.g. chart.yaml or csproj) and the latest published version.

In these articles you may see more casting (e.g. [SemanticVersion]) than is necessary however I find it’s clearer to include it when dealing with non-basic types likes strings and ints.

As with my C# TDD articles, this will also have a GitHub repository made available. When it is, the link to it will appear here.

These articles are evolving so, particularly the third part, will change over time.

Pester Installing or Updating

I’m using Windows 11, PowerShell Core 7 and VS Code for this project so any instructions will be for that platform.

To install the latest version of Pester and ensure any existing versions don’t cause issues, including the default v3 installed with Windows, run the following command:

Install-Module -Name Pester -Force
Import-Module Pester

The second command is to force the current session to pick up the latest version of Pester.

First Test – Return 0.0.1 If No Version Exists

The first test should return 0.0.1 if no existing version is found and the csproj file version is 0.0.1 or less, no pre-release tag is specified and no build number is specified.

Failing Test

using namespace System.Management.Automation

BeforeAll {
  . $PSScriptRoot/Get-NextVersion.ps1
}

Describe 'Get-NextVersion' {
  It 'Return 0.0.1 when no meaningful version exists' {
    # Arrange
    $fileVersion = [SemanticVersion]"0.0.0"
    $publishedVersion = [SemanticVersion]"0.0.0"
    $preReleaseLabel = ""
    $buildLabel = ""

    $expectedVersion = [SemanticVersion]"0.0.1"

    # Act
    $newVersion = [SemanticVersion](Get-NextVersion -FileVersion $fileVersion -PublishedVersion $publishedVersion -PreReleaseLabel $preReleaseLabel -BuildLabel $buildLabel)

    # Assert
    $newVersion | Should -Be $expectedVersion
  }
}

To make this test run, an empty function definition should be added to the main ps1 file:

function Get-NextVersion {

}

The tests can then be ran using the following command:

Invoke-Pester -Output Detailed .\Get-NextVersion.Tests.ps1

Making the Test Green

To make the test pass, the easiest thing to do is simply return the expected version of 0.0.1 so that is what we’ll do.

function Get-NextVersion {
  Write-Output "0.0.1"
}

Second Test – Returning One Patch Higher Than The Latest Published Package

This test will ensure that any version number has a one higher patch number than the latest published patch. At this stage, the pre-release tag and build number are being ignored.

Failing Test

It 'Return one patch higher than existing published version' {
  # Arrange
  $fileVersion = [SemanticVersion]"0.0.0"
  $nugetVersion = [SemanticVersion]"0.3.1"
  $preReleaseLabel = ""
  $buildLabel = ""

  $expectedVersion = [SemanticVersion]"0.3.2"

  # Act
  $newVersion = [SemanticVersion](Get-NextVersion -FileVersion $fileVersion -PublishedVersion $publishedVersion -PreReleaseLabel $preReleaseLabel -BuildLabel $buildLabel)

  # Assert
  $newVersion | Should -Be $expectedVersion
}

Making the Test Green

To make the test green, we need to add a parameter to the Get-NextVersion function and then use it to add suitable logic. We should only add the needed parameter from the ones currently being passed.

The resulting function code will look like this:

using namespace System.Management.Automation

function Get-NextVersion {
  param (
    [SemanticVersion] $PublishedVersion
  )

  $newVersion = [SemanticVersion]"0.0.1"
  $notSet = [SemanticVersion]"0.0.0"

  if ($NuGetVersion -ne $notSet) {
    $newVersion = [SemanticVersion]::new($PublishedVersion.Major, $PublishedVersion.Minor, $PublishedVersion.Patch + 1, $PublishedVersion.PreReleaseLabel, $PublishedVersion.BuildLabel)
  }

  Write-Output $newVersion
}

This will work but could mean a lot of repeated and similar code each time one or more parts of the version change. Therefore, it would make sense to create a function for building up a new version, only updating those values which have changed.

Third Test – Function To Build New SemanticVersion When All Parameters Are Set

This test will pass in all new values for a SemanticVersion object and return a new version. This is effectively replicating the constructor but test four will handle no parameters being passed making this new function useful as only the changed values will need to be passed.

Failing Test

Describe 'Get-UpdatedSemanticVersion' {
  It 'Returns completely new version when all parameters are set' {
    # Arrange
    $currentVersion = [SemanticVersion]"1.2.3-dev+build1"
    $expectedVersion = [SemanticVersion]"4.5.6-new+build2"

    # Act
    $newVersion = [SemanticVersion](Get-UpdatedSemanticVersion $currentVersion -MajorVersion 4 -MinorVersion 5 -PatchVersion 6 -PreReleaseLabel "new" -BuildLabel "build2")

    # Assert
    $newVersion | Should -Be $expectedVersion
  }
}

Also, so the test fails (rather than fails to execute), add the following method to Get-NextVersion.ps1:

function Get-UpdatedSemanticVersion {
}

Making the Test Green

Making the test pass when all the parameters are specified is relatively straight forward:

function Get-UpdatedSemanticVersion {
  param (
    [Parameter(Mandatory, Position=0)] [SemanticVersion] $CurrentVersion,
    [int] $MajorVersion,
    [int] $MinorVersion,
    [int] $PatchVersion,
    [string] $PreReleaseLabel,
    [string] $BuildLabel
  )

  $newVersion = [SemanticVersion]::new($MajorVersion, $MinorVersion, $PatchVersion, $PreReleaseLabel, $BuildLabel)

  Write-Output $newVersion
}

But to make this test useful, only updating the new values passed is key so a new test is needed.

Fourth Test – Return Current Version When No Parameters Passed

Failing Test

  It 'Returns current version when all parameters except CurrentVersion are not set' {
    # Arrange
    $currentVersion = [SemanticVersion]"1.2.3-dev+build1"
    $expectedVersion = [SemanticVersion]"1.2.3-dev+build1"

    # Act
    $newVersion = [SemanticVersion](Get-UpdatedSemanticVersion $currentVersion)

    # Assert
    $newVersion | Should -Be $expectedVersion
  }

Making the Test Green

We need to check if all parameters were passed or not. We can use $PSBoundParameters to do this by checking each parameter name.

function Get-UpdatedSemanticVersion {
  param (
    [Parameter(Mandatory, Position=0)] [SemanticVersion] $CurrentVersion,
    [int] $MajorVersion,
    [int] $MinorVersion,
    [int] $PatchVersion,
    [string] $PreReleaseLabel,
    [string] $BuildLabel
  )

  if (($PSBoundParameters.ContainsKey('MajorVersion') -eq $false) -and
      ($PSBoundParameters.ContainsKey('MinorVersion') -eq $false) -and 
      ($PSBoundParameters.ContainsKey('PatchVersion') -eq $false) -and 
      ($PSBoundParameters.ContainsKey('PreReleaseLabel') -eq $false) -and
      ($PSBoundParameters.ContainsKey('BuildLabel') -eq $false)) {
    $newVersion = $CurrentVersion
  } else {
    $newVersion = [SemanticVersion]::new($MajorVersion, $MinorVersion, $PatchVersion, $PreReleaseLabel, $BuildLabel)
  }

  Write-Output $newVersion
}

Fifth Test – Removing Labels

This fifth test will be testing two things in a way – removing the labels when they’re already defined and that only some parameters can be specified for updating.

Failing Test

  It 'Returns current version with labels when labels are set to blanks strings' {
    # Arrange
    $currentVersion = [SemanticVersion]"1.2.3-dev+build1"
    $expectedVersion = [SemanticVersion]"1.2.3"

    # Act
    $newVersion = [SemanticVersion](Get-UpdatedSemanticVersion $currentVersion -PreReleaseLabel "" -BuildLabel "")

    # Assert
    $newVersion | Should -Be $expectedVersion
  }

Making the Test Green

We’ll do a check per parameter and set the new value to use to the current version if a new one isn’t specified.

function Get-UpdatedSemanticVersion {
  param (
    [Parameter(Mandatory, Position=0)] [SemanticVersion] $CurrentVersion,
    [int] $MajorVersion,
    [int] $MinorVersion,
    [int] $PatchVersion,
    [string] $PreReleaseLabel,
    [string] $BuildLabel
  )

  if ($PSBoundParameters.ContainsKey('MajorVersion') -eq $false) {
    $MajorVersion = $CurrentVersion.Major
  }

  if ($PSBoundParameters.ContainsKey('MinorVersion') -eq $false) {
    $MinorVersion = $CurrentVersion.Minor
  }

  if ($PSBoundParameters.ContainsKey('PatchVersion') -eq $false) {
    $PatchVersion = $CurrentVersion.Patch
  }

  if ($PSBoundParameters.ContainsKey('PreReleaseLabel') -eq $false) {
    $PreReleaseLabel = $CurrentVersion.PreReleaseLabel
  }

  if ($PSBoundParameters.ContainsKey('BuildLabel') -eq $false) {
    $BuildLabel = $CurrentVersion.BuildLabel
  }

  $newVersion = [SemanticVersion]::new($MajorVersion, $MinorVersion, $PatchVersion, $PreReleaseLabel, $BuildLabel)

  Write-Output $newVersion
}

Refactoring Get-NextVersion to Use This New Method

The code change is pretty simple and then, once made, re-run the tests to make sure all responses are working as expected.

Change the following line:

$newVersion = [SemanticVersion]::new($PublishedVersion.Major, $PublishedVersion.Minor, $PublishedVersion.Patch + 1, $PublishedVersion.PreReleaseLabel, $PublishedVersion.BuildLabel)

To the following:

$newVersion = [SemanticVersion](Get-UpdatedSemanticVersion $PublishedVersion -PatchVersion ($PublishedVersion.Patch + 1))

Sixth Test – New Version Is One Version Higher Than the Higher of File Version and NuGet Version

This test is about ensuring the next version is higher than any existing version. Three data sets will be tried; one where file version is higher than published version, one where it’s lower and one where it’s the same.

Failing Test

  It 'New version is one version higher (<expectedVersion>) than the higher of file version (<fileVersion>) and published version (<publishedVersion>)' -ForEach @(
    @{ FileVersion = "1.0.0"; PublishedVersion = "1.0.3"; ExpectedVersion = "1.0.4" }
    @{ FileVersion = "1.1.0"; PublishedVersion = "1.0.3"; ExpectedVersion = "1.1.1" }
    @{ FileVersion = "1.2.0"; PublishedVersion = "1.2.0"; ExpectedVersion = "1.2.1" }
  ) {
    # Arrange
    $fileVersion = [SemanticVersion]$fileVersion
    $publishedVersion = [SemanticVersion]$publishedVersion
    $preReleaseLabel = ""
    $buildLabel = ""

    $expectedVersion = [SemanticVersion]$expectedVersion

    # Act
    $newVersion = [SemanticVersion](Get-NextVersion -FileVersion $fileVersion -PublishedVersion $publishedVersion -PreReleaseLabel $preReleaseLabel -BuildLabel $buildLabel)

    # Assert
    $newVersion | Should -Be $expectedVersion
  }

You’ll notice that there are now 8 tests passing/failing even though we’ve only written six. That is because the -ForEach is effectively creating multiple variants of one test.

Making the Test Green

We’ll need to add a new parameter (FileVersion) and use this to determine what the highest new version is. By doing independent checks and updating newVersion as appropriate, the code is easier to read. newVersion is now defaulting to 0.0.0 so that the patch can also be incremented. We’ll see in later tests that we may not always want this.

function Get-NextVersion {
  param (
    [SemanticVersion] $FileVersion,
    [SemanticVersion] $PublishedVersion
  )

  $newVersion = [SemanticVersion]"0.0.0"
  $notSet = [SemanticVersion]"0.0.0"

  if ($FileVersion -ne $notSet -and $FileVersion -gt $newVersion) {
    $newVersion = $FileVersion
  }
  if ($PublishedVersion -ne $notSet -and $PublishedVersion -gt $newVersion) {
    $newVersion = $PublishedVersion
  }

  $newVersion = [SemanticVersion](Get-UpdatedSemanticVersion $newVersion -PatchVersion ($newVersion.Patch + 1))

  Write-Output $newVersion
}

Seventh Test

As this article is getting quite long, I’ll stop here and continue in the next article which will see more rules added and some mocking done.

Test Driven Development (TDD) – C# – Blazor Server with bUnit (part 4 of 5)

In this third article on building an app using TDD, we’ll build a blog list component and add it to the default page. We’ll be using bUnit for the testing of this component.

Fourth Test – Blog List UI When Empty

The first test we’ll write will cover a blog list when no values are available. We’ll need to mock the BlogPost class created in the previous articles and we’ll do this by creating an interface of the class.

The test will look like this:

[Fact]
public void ShowNoBlogsFoundOnEmptyList()
{
    // Arrange
    IEnumerable<BlogPostEntry> blogPostEntries = new List<BlogPostEntry>();
    Mock<IBlogPost> blogPost = new();
    blogPost.Setup(method => method.ListAsync()).Returns(Task.FromResult(blogPostEntries));

    Services.AddSingleton<IBlogPost>(blogPost.Object);
    var cut = RenderComponent<BlogList>();

    // Act

    // Assert
    cut.Find("p").MarkupMatches("<p>There are no blogs to display.</p>");
}

This will require bUnit and Microsoft.Extensions.DependencyInjection installing. You will also need to create a project reference to Demo.Tdd.Contentful. The class should inherit from TestContext.

The next step is to create the interface for BlogPost and inherit from it. In Visual Studio, you can do this by going into the class, right clicking on the class name, choosing “Quick Actions and Refactorings…” and choosing the “Extract Interface” option. The default values should be sufficient. Click OK and the interface will be created and the current class set to inherit from it.

In the UI project, create a “Shared” folder and within it create a new Razor component called “BlogList.razor”.

Making the Test Green

Populate the BlogList with the following:

<p>There are no blogs to display.</p>

@code {

}

This will satisfy the test but is obviously never going to return anything dynamic and isn’t using the mocked object we’ve created in the test, etc… As done previously, we’ll create a new test that handles the scenario where data exists and make use of the mock we’ve created.

Fifth Test – Listing Blogs

This test will require a few changes to make it pass and will require a couple of properties adding to BlogPostEntry just to make the code compile. This test should also spawn other tests such as one that says the ListAsync method returns title, summary and article when called.

Failing Test

The new test should look like:

[Fact]
public void ListMostRecentBlogs()
{
    // Arrange
    IEnumerable<BlogPostEntry> blogPostEntries = new List<BlogPostEntry>() { new BlogPostEntry() { Title = "Test blog article", Summary = "A bit of blurb with some __bold__ text." } };
    Mock<IBlogPost> blogPost = new();
    blogPost.Setup(method => method.ListAsync()).Returns(Task.FromResult(blogPostEntries));

    Services.AddSingleton<IBlogPost>(blogPost.Object);
    var cut = RenderComponent<BlogList>();

    // Act
    cut.WaitForState(() => cut.Find("ul") is not null);

    // Assert
    var ul = cut.Find("ul");
    ul.Children[0].MarkupMatches("<li><b>Test blog article</b><p>A bit of blurb with some <strong>bold</strong> text.</p></li>");
}

To make the code compile, add the following code to the BlogPostEntry class in Demo.Tdd.Contentful:

public string? Title { get; set; }
public string? Summary { get; set; }

Making the Test Green

To make both tests pass, the BlogPost component needs to be updated to accept the injected IBlogPost and iterate through any results. As we are storing Markdown in Contentful, we’ll need to use a package to convert this to HTML. The package I recommend is Markdig. Also, to make Blazor render the HTML as HTML and not a string, we’ll cast the value to MarkupString.

Update the BlogList component as follows:

@using Demo.Tdd.Contentful;
@using Demo.Tdd.Contentful.Models;
@using Markdig;

@inject IBlogPost _blogPost;

@if (blogList is null || !blogList.Any())
{
    <p>There are no blogs to display.</p>
}
else
{
    <ul>
    @foreach (var blogEntry in blogList)
    {
        <li>
            <b>@blogEntry.Title</b>
            @((MarkupString)Markdown.ToHtml(blogEntry.Summary ?? ""))
        </li>
    }
    </ul>
}

@code {
    IEnumerable<BlogPostEntry>? blogList = null;

    protected override async Task OnInitializedAsync()
    {
        blogList = await _blogPost.ListAsync();
    }
}

The test will now pass but the code will fail to run.

Sixth Test – Add BlogList to Index Page

The final UI test for this series of blogs is to add the component to the Index.razor page. We’ll write a test for this to confirm the component is present.

Failing Test

Firstly, create a new class within the UI testing project called HomePageShould, make the class public and inherit from TestContext. Then populate it with the following test:

[Fact]
public void IncludeBlogListComponent()
{
    // Arrange
    Mock<IBlogPost> blogPost = new();
    Services.AddSingleton<IBlogPost>(blogPost.Object);
    var put = RenderComponent<Pages.Index>();

    // Act

    // Assert
    Assert.True(put.HasComponent<BlogList>());
}

No additional changes are required to make the code compile and test fail.

Making the Test Green

Firstly, in the _Imports.razor page, add the following line:

@using Demo.Tdd.Ui.Shared

This removes the need to add this to each page using a shared component.

The add the following to Index.razor:

<BlogList></BlogList>

This will result in a passing test. The code won’t run but we’ll cover that in the refactor stage.

Refactor to Make Code Run

To allow the code to run, in Program.cs, under the other service declarations, add the following:

builder.Services.AddHttpClient();
builder.Services.AddSingleton<IBlogPost, BlogPost>();

Be aware that this will only work if no errors occur (i.e. the API key is correct and the space and environment values are correct. Also, as no code has been added to BlogPost.cs to return the title and summary, if you’ve added a blog entry, you won’t see the title or summary.

Seventh Test – Handle an Error from the API

The final tests we’ll write are to return an empty list if an error occurs and to return values. This will be covered in the next blog article.

Test Driven Development (TDD) – C# – Finishing the App (part 5 of 5)

This fifth and final article will add some error handling to the blog list retrieval and also populate the BlogPostEntry object with values from the API.

Seventh Test – Handle Errors

This is a relatively simple test but does require an additional option creating in our MockFactories class.

Failing Test

Firstly, let’s create the failing test in Demo.Tdd.Contentful.Tests:

[Fact]
public async void ReturnEmptyListIfTimeoutOccurs()
{
    // Arrange
    IHttpClientFactory httpClientFactory = MockFactories.GetTimingOutClient();
    var sut = new BlogPost(httpClientFactory, Constants.GetContentfulConfig());

    // Act
    var blogPosts = await sut.ListAsync();

    // Assert
    blogPosts.Should().NotBeNull();
    blogPosts.Should().BeEmpty();
}

We will specifically be throwing a TimeoutException but we’ll write the code to catch any exception when we come to implement it.

Making the Test Green

To make the test green, we’ll wrap the API call in a try/catch. We’ll also need to move the declaration of posts to outside the try/catch.

string? posts = null;

try
{
    posts = await _httpClient.GetStringAsync($"/spaces/{_config.SpaceId}/environments/{_config.Environment}/entries?access_token={_config.ApiKeys?.PublishedContent}");
} 
catch
{
    return new List<BlogPostEntry>();
}

You will now also find that the UI project will run successfully, even if an error occurs.

Eighth Test – Populate Title, Summary and Article from API

This final test will check the data coming back from the API is populating the BlogPostEntry object within the list of blogs returned.

Failing Test

The test checks that known values (pulled from the constant) are assigned to the properties of the first blog entry in the list.

[Fact]
public async void AListOfBlogsHasPopulatedValues()
{
    // Arrange
    IHttpClientFactory httpClientFactory = MockFactories.GetStringClient(Constants.RESULT_LIST_POPULATED_TWO);
    var sut = new BlogPost(httpClientFactory, Constants.GetContentfulConfig());

    // Act
    var results = await sut.ListAsync();

    // Assert
    results.Should().NotBeNullOrEmpty();
    results.First().Title.Should().Be("Test blog article");
    results.First().Summary.Should().Be("This is my __test__ blog article summary.");
    results.First().Article.Should().Be("This is the body __with bold__ of my article.");
}

To get the code to compile, add the missing “Article” property to BlogPostEntry:

public string? Article { get; set; }

Making the Test Green

To make the test pass, we need to modify the code within the foreach of the ListAsync method as shown below:

var fields = (JsonElement)blogPostEntry["fields"];

blogPosts.Add(new BlogPostEntry()
{
    Title = fields.GetProperty("title").GetString() ?? "",
    Summary = fields.GetProperty("summary").GetString() ?? "",
    Article = fields.GetProperty("article").GetString() ?? ""
});

If you run the UI again, making sure the spaceId, environment and API keys are set correctly in appsettings.json and user secrets, then the title and summary will be displayed.

Conclusion

This is obviously not a fully complete application but hopefully these articles have helped illustrate the TDD process, provide a practical demo and potentially some ways to mock or stub items.

Test Driven Development (TDD) – C# – Faking the Configuration (part 3 of 5)

This follow up article to part ii of my TDD learning project will cover using the Options pattern (with IOptions) to read your configuration as well as User Secrets for sensitive storage of keys.

Third Test – Configuration

This test will use a code-generated version of an IOptions implementation which will return fixed responses (a stub) that, when ran in the normal code, will use dependency injection and the reading of configuration files to populate.

Test Code

The test code is relatively straightforward but will make use of a couple of custom methods to keep the test readable.

[Fact]
public async void ReadValuesFromConfigurationToCallAPI()
{
    // Arrange
    Mock<HttpMessageHandler> httpMessageHandler;
    IHttpClientFactory httpClientFactory = MockFactories.GetMockMessageHandler(Constants.RESULT_LIST_EMPTY, out httpMessageHandler);
    var sut = new BlogPost(httpClientFactory, Constants.GetContentfulConfig());
    string expectedQueryList = "https://api.baseurl.com/spaces/SPACE-ID/environments/ENVIRONMENT/entries?access_token=MY-ACCESS-TOKEN";

    // Act
    await sut.ListAsync();

    // Assert
    httpMessageHandler.Protected().Verify(
        "SendAsync",
        Times.Once(),
        ItExpr.Is<HttpRequestMessage>(entry => entry.RequestUri.AbsoluteUri == expectedQueryList),
        ItExpr.IsAny<CancellationToken>()
        );
}

Also note how the expected URL is a complete URL and is a single string (i.e. no path combining, string formatting, etc…). This test is just testing what URL is called and doesn’t care about what might come back.

Mocked MessageHandler Method

This is used to retrieve the mock used to create the HttpClient’s handler. We retrieve this so that we can verify what URL was called. This method will be added to our MockFactories class.

public static IHttpClientFactory GetMockMessageHandler(string returnValue, out Mock<HttpMessageHandler> httpMessageHandler)
{
    httpMessageHandler = new();
    Mock<IHttpClientFactory> httpClientFactory = new();

    httpMessageHandler.Protected().Setup<Task<HttpResponseMessage>>(
            "SendAsync",
            ItExpr.IsAny<HttpRequestMessage>(),
            ItExpr.IsAny<CancellationToken>()
        )
        .ReturnsAsync(new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(returnValue) });

    HttpClient httpClient = new(httpMessageHandler.Object);
    httpClientFactory.Setup(factory => factory.CreateClient(It.IsAny<string>())).Returns(httpClient);

    return httpClientFactory.Object;
}

Models for Config

Next, we need to create some models to store the configuration we’ll be using. Personally, I like to include the needed classes in a single file, named to match the top level class.

public class ContentfulConfig
{
    public string? BaseUrl { get; set; }
    public string? SpaceId { get; set; }
    public string? Environment { get; set; }
    public ContentfulConfigApiKeys? ApiKeys { get; set; }
}

public class ContentfulConfigApiKeys
{
    public string? PublishedContent { get; set; }
    public string? PreviewContent { get; set; }
}

Fixed Configuration Values

Next, in the Constants class, create a new method that will create an implementation of IOptions with fixed values:

public static IOptions<ContentfulConfig> GetContentfulConfig()
{
    ContentfulConfig contentfulConfig = new()
    {
        BaseUrl = "https://api.baseurl.com",
        SpaceId = "SPACE-ID",
        Environment = "ENVIRONMENT",
        ApiKeys = new ContentfulConfigApiKeys() { PublishedContent = "MY-ACCESS-TOKEN" }
    };

    return Options.Create(contentfulConfig);
}

Fix Compile Errors

The final changes are relating to the new parameter we’ll need to pass to the constructor. In the new test, you’ll see it’s already included as a second parameter but the BlogPost class doesn’t have an overload for this method yet. As this is a brand new app, we will update the existing constructor’s signature. If this was an established app, a new overload may be a better/safer option.

Update the signature for BlogPost to be:

public BlogPost(IHttpClientFactory httpClientFactory, IOptions<ContentfulConfig> config)

Finally, update the two original tests to pass in the new config:

var sut = new BlogPost(httpClientFactory, Constants.GetContentfulConfig());

Making the Test Green

Three changes are needed to make the test go green. The first is to get the config available throughout the class, the second is in the constructor and the third in the ListAsync method.

Start by creating a private read-only variable called _config:

private readonly ContentfulConfig _config;

And then set the value of this in the constructor (place it above the existing lines of code):

_config = config.Value;

In the constructor, we’ll change the code from a fixed base address to one read from config by swapping out the BaseAddress assignment from new Uri("https://cdn.contentful.com") to new Uri(_config.BaseUrl ?? "").

Next, swap out the URL in the GetStringAsync call in ListAsync and replace it with the following:

$"/spaces/{_config.SpaceId}/environments/{_config.Environment}/entries?access_token={_config.ApiKeys?.PublishedContent}"

Making Application Work

The work done so far allows the tests to run but changes must also be made to the main application.

Firstly, add the following to the appsettings.json in Demo.Tdd.Ui in the root element:

  "Contentful": {
    "BaseUrl": "https://cdn.contentful.com",
    "SpaceId": "b27mtzlwgnec",
    "Environment": "production",
    "ApiKeys": {
      "PublishedContent": "",
      "PreviewContent": ""
    }
  }

Notice the blank ApiKey sub-values. These will be store in User Secrets to prevent them getting checked into source control.

To add the user secrets, right click on the Demo.Tdd.Ui project and choose the “Manage User Secrets” option. Paste in the following, populating your keys as indicated.

{
  "Contentful": {
    "ApiKeys": {
      "PublishedContent": "your-published-content-key",
      "PreviewContent": "your-preview-content-key"
    }
  }
}

Finally, in Program.cs, we need to set up dependency injection for the configuration. Add the following line below the current builder.Services... lines:

{
  "Contentful": {
    "ApiKeys": {
      "PublishedContent": "your-published-content-key",
      "PreviewContent": "your-preview-content-key"
    }
  }
}

Finally, in Program.cs, we need to set up dependency injection for the configuration. Add the following line below the current builder.Services... lines:

builder.Services.Configure<ContentfulConfig>(builder.Configuration.GetSection("Contentful"));

You will need to add a project reference from the UI to the Contentful class project for the code to compile.

Fourth Test – Blog List Component

In the next blog article, we’ll build a blog list component and test it using bUnit.

Test Driven Development (TDD) – C# – Introduction and Setup (part 1 of 5)

This series of articles documents what I have learnt building the initial stages of a relatively simple application to retrieve information from Contentful’s API (I know they have an SDK, but handling HttpClient within the context of TDD was part of what I wanted to learn) and then displaying it on a Blazor Server frontend.

I’m documenting this, for the most part, in the order I did it. Due to the size of the topic, I’m splitting it into multiple parts with links to the next article included at the bottom of each article.

The primary purpose of this project was to learn and try TDD but, ultimately, I also wish to move my blog off WordPress so this is the first building block in developing that real-world application.

The code from this blog is available on GitHub with branches covering the various stages to allow you to follow along if desired. The default branch will only include the initial projects and NuGet packages referenced in “Initial Setup” below.

TDD TL;DR

I am following the commonly used red, green, refactor approach normally used with TDD. This basically means writing a test (it fails/shows as red in test runners because the code isn’t implemented yet), writing enough code to make the test pass (green in test runners) then refactor to improve the test or code without fundamentally changing either. This process is then repeated for each new function or requirement to be added.

For the first (red) stage, some code will be needed to make the test code actually compile (e.g. creating a basic class with a method stub) but nothing more should be done. Throwing a not implemented exception in the added methods is a good way to do this.

Getting Started

This section covers an overview of what the articles and, ultimately, code will cover and the initial setup of the solution. Whilst I’ve listed all the technologies used, I did not install all of these at the outset as I wanted to build out the solution iteratively as would be done on a larger project.

Frameworks, Tools and Technologies Used

All versions used are the latest available versions at the time the projects were created or packages installed.

  • .NET 7
  • Blazor Server
  • bUnit
  • Contentful
  • FluentAssertions
  • Markdig
  • Moq
  • Visual Studio 2022
  • xUnit

Contentful

This project will be using Contentful for it’s API-based data source. You can set up a free account to allow you to follow along with the process. To make life easy, set up a “Blog Article” content model and add three fields to it:

  • Title (Short text)
  • Summary (Long text)
  • Article (Long text)

You can then create a piece of content of the type “Blog Article” for testing later.

Initial C# Setup

Firstly, I created a new solution (Demo.Tdd) with four projects:

  • Blazor Server Project (Empty Template) – Demo.Tdd.Ui
  • Class library (to do connect to Contentful API) – Demo.Tdd.Contentful
  • 2 xUnit projects – one for each of the above – as above with .Tests on the end

I then applied any updated and then installed Moq and FluentAssertions into both xUnit projects. Finally, I created project references between the respective test and code projects.

For the most part, I won’t specifically mention some changes such as switching to top-level namespaces or tidying up using statements however you will see these changes in the GitHub repo code.

The Tests

In total, eight tests will be created throughout these articles. For convenience, I’ve listed these below and put direct links to both the article and the GitHub red-state branch (i.e. test written but failing).

  1. Empty List (Article | GitHub)
  2. Populated List (Article | GitHub)
  3. Configuration (Article | GitHub)
  4. Blog List UI When Empty (Article | GitHub)
  5. Listing Blogs (Article | GitHub)
  6. Add BlogList to Index Page (Article | GitHub)
  7. Handle Errors (Article | GitHub)
  8. Populate Title, Summary and Article from API (Article | GitHub)

Test Driven Development (TDD) – C# – API Calling (part 2 of 5)

First Test – Empty List

The first test I decided to write was to have an empty result set from the API return an empty list of blog post entries. This would involve the code connecting out to the API, retrieving data and then converting that to a suitable object structure.

I will be using the arrange/act/assert structure for all tests, skipping any section where necessary.

Whilst ultimately the code will need to read settings from a config file, call out to an API, handle working and non-working conditions and return a list, the first test does not need to consider all these scenarios. Adding in some of these scenarios may (will!) require code refactoring but that is to be expected with any iterative design.

Test Naming

The naming convention I’m using is to name the testing class after the class or component being tested with “Should” on the end (e.g. if the code class is called BlogPost then the test class would be called BlogPostShould).

The individual tests then describe what is being tested but is human readable, potentially in broken English, when including the test name e.g. ReturnAnEmptyListWhenNoBlogsExist would read as BlogPost should return an empty list when no blogs exist.

First Test Breakdown

The first test is covering the scenario of connecting to the Contentful API, retrieving an empty list of blogs and returning an empty list of blog entry objects. It’s not handling errors, checking Contentful or HttpClient works or even defining what fields will be part of the blog entry class.

HttpClient Mocking

I had already decided to use IHttpClientFactory for getting my HttpClient instance so began by looking at how to mock the API response. After some Googling and pulling answers from a few sources, I came up with the method outlined below. I placed this in a static class called MockFactories to make consuming it easier.

public static IHttpClientFactory GetStringClient(string returnValue)
{
    Mock<HttpMessageHandler> httpMessageHandler = new();
    Mock<IHttpClientFactory> httpClientFactory = new();

    httpMessageHandler.Protected().Setup<Task<HttpResponseMessage>>(
            "SendAsync",
            ItExpr.IsAny<HttpRequestMessage>(),
            ItExpr.IsAny<CancellationToken>()
        )
        .ReturnsAsync(new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(returnValue) });

    HttpClient httpClient = new(httpMessageHandler.Object);
    httpClientFactory.Setup(factory => factory.CreateClient(It.IsAny<string>())).Returns(httpClient);

    return httpClientFactory.Object;
}

This will return the value of returnValue and an OK (200) response, regardless of what URL is requested.

Test Code

The first test code was pretty straightforward thanks to the above mocking class. Realising some constants will be needed, a class was created called “Constants” which will hold any constants. The below test code won’t compile at this stage but it will shortly…

[Fact]
public async void ReturnAnEmptyListWhenNoBlogsExist()
{
    // Arrange
    IHttpClientFactory httpClientFactory = MockFactories.GetStringClient(Constants.RESULT_LIST_EMPTY);
    var sut = new BlogPost(httpClientFactory);

    // Act
    var results = await sut.ListAsync();

    // Assert
    results.Should().NotBeNull();
    results.Should().BeEmpty();
}

This code won’t compile because the constant (class) doesn’t exist, there is no class of “BlogPost” and therefore no method called “ListAsync” on such a class.

Filling the Gaps

To make the test compile, some gaps need to be plugged. We’ll tackle these in the order outlined above.

First, create a static class called Constants and in it and a string constant called RESULT_LIST_EMPTY which includes an API response example of an empty list. What this will look like will vary depending on the API you’re referencing. Contentful’s documentation is pretty good and includes a live test page that you can use to test API calls and see what an example response will look like. In all cases, it’s the structure we’re after so you may wish to review any example data to make sure sensitive content is replaced (e. IDs and especially personal data).

For an empty result set, the string constant will look like this:

public const string RESULT_LIST_EMPTY = """
    {
        "sys": {
        "type": "Array"
        },
        "total": 0,
        "skip": 0,
        "limit": 100,
        "items": []
    }
    """;

The """ is a C# 11 feature called raw string literals. This makes it much easier to have constants that contain quotes, etc…

Next thing to create is a class called BlogPost (we’ll skip an interface for now). This needs a constructor that accepts an IHttpClientFactory as a parameter.

Technically, at this stage, an async method isn’t needed as it’s not actually making an API call but it will be very soon so will create the method in that way:

public async Task<IEnumerable<BlogPostEntry>> ListAsync()
{
    throw new NotImplementedException();
}

This will now cause another compile issue so create a new model class called called BlogPostEntry. For now, it doesn’t need any properties.

Compiling the code and running the tests should now work and give a failing test.

Making the Test Green

To make the test pass, the easiest thing to do is make the ListAsync method return an empty array so we’ll do this now.

public async Task<IEnumerable<BlogPostEntry>> ListAsync()
{
    return new List<BlogPostEntry>();
}

At this point, two things could happen, the code could be refactored to make use of the IHttpClientFactory that was passed in or a new test could be written to check the code returns results when some items are returned from the API. The advantage of the latter approach is that the new code is a result of a new test which means the change is being more thoroughly tested.

Second Test – Populated List

As described above, this test covers the scenario when results are present in the API response.

Failing Test

The code for the second test is similar to the first test but with a different constant and different second expectation/assert.

[Fact]
public async void ReturnAListOfBlogs()
{
    // Arrange
    IHttpClientFactory httpClientFactory = MockFactories.GetStringClient(Constants.RESULT_LIST_POPULATED_TWO);
    var sut = new BlogPost(httpClientFactory);

    // Act
    var results = await sut.ListAsync();

    // Assert
    results.Should().NotBeNullOrEmpty();
    results.Should().HaveCount(2);
}

To make this compile, create a second string constant in the Constants class with the following value:

public const string RESULT_LIST_POPULATED_TWO = """
    {
        "sys": {
        "type": "Array"
        },
        "total": 2,
        "skip": 0,
        "limit": 100,
        "items": [
        {
            "metadata": {
            "tags": []
            },
            "sys": {
            "space": {
                "sys": {
                "type": "Link",
                "linkType": "Space",
                "id": "b27mtzlwgnec"
                }
            },
            "id": "1dP03LAJlww9kikCDLFTXL",
            "type": "Entry",
            "createdAt": "2022-12-26T16:41:06.408Z",
            "updatedAt": "2022-12-26T17:08:57.292Z",
            "environment": {
                "sys": {
                "id": "production",
                "type": "Link",
                "linkType": "Environment"
                }
            },
            "revision": 2,
            "contentType": {
                "sys": {
                "type": "Link",
                "linkType": "ContentType",
                "id": "blogArticle"
                }
            },
            "locale": "en-US"
            },
            "fields": {
            "title": "Test blog article",
            "summary": "This is my __test__ blog article summary.",
            "article": "This is the body __with bold__ of my article."
            }
        },
        {
            "metadata": {
            "tags": []
            },
            "sys": {
            "space": {
                "sys": {
                "type": "Link",
                "linkType": "Space",
                "id": "b27mtzlwgnec"
                }
            },
            "id": "2dP03LAJlww9kikMBLBNPQ",
            "type": "Entry",
            "createdAt": "2022-12-27T16:41:06.408Z",
            "updatedAt": "2022-12-27T17:08:57.292Z",
            "environment": {
                "sys": {
                "id": "production",
                "type": "Link",
                "linkType": "Environment"
                }
            },
            "revision": 2,
            "contentType": {
                "sys": {
                "type": "Link",
                "linkType": "ContentType",
                "id": "blogArticle"
                }
            },
            "locale": "en-US"
            },
            "fields": {
            "title": "Another test blog article",
            "summary": "This is my __second__ blog article summary.",
            "article": "This body __also has bold__ in my article."
            }
        }
        ]
    }
    """;

The code will now compile but fail the second test.

Making the Test Green

Firstly, let’s get the parameter injected into the constructor available as a private variable. As it’s the HttpClient we want to have available, we’ll create a private variable for it:

private readonly HttpClient _httpClient;

Then add the following to the constructor to create the HttpClient and specify a base address (which we’ll hard code for now):

public BlogPost(IHttpClientFactory httpClientFactory)
{
    _httpClient = httpClientFactory.CreateClient();
    _httpClient.BaseAddress = new Uri("https://cdn.contentful.com");
}

Whilst we aren’t calling anything for real yet, a BaseAddress is needed so we might as well set it to the real one.

We now need to call the mock HttpClient. We will need the real URL to call to make the actual code work but, for now, we’ll just populate a placeholder URL as we’re not currently running the code other than as part of a test.

public async Task<IEnumerable<BlogPostEntry>> ListAsync()
{
    string? posts = await _httpClient.GetStringAsync("/placeholder-path");

    var response = JsonSerializer.Deserialize<ContentfulResponse>(posts);

    if (response is null || response.items is null || !response.items.Any())
    {
        return new List<BlogPostEntry>();
    }

    List<BlogPostEntry> blogPosts = new();

    foreach (var blogPostEntry in response.items)
    {
        blogPosts.Add(new BlogPostEntry());
    }

    return blogPosts;
}

Note the ContentfulResponse in the JSON deserialize command, which is currently not defined. Create a new class for this with the following content:

internal class ContentfulResponse
{
    public int total { get; set; }
    public List<Dictionary<string, object>>? items { get; set; }
}

Note that the code we have written will return a list with empty elements. To remedy this, we’d right another test to make sure content is populated. This could be combined into this test if desired but many simple tests can be helpful when changing or refactoring code as you get a narrower target to look for issues. It also allows for better naming of your tests.

Refactor The Code

The current test may pass but the code won’t actually work due to the placeholder API URL so we’ll refactor the code slightly to include the real address. This example won’t work for you as space ID, environment and access token will all be different (these aren’t real tokens, obviously!) but if you insert the equivalent into your code, you’ll find the code will run and work, not that we have a visual way to test it yet but we’ll get to that…

Replace:

_httpClient.GetStringAsync("/placeholder-path");

With:

_httpClient.GetStringAsync("/spaces/b27mtzlwgnec/environments/production/entries?access_token=dskuhg87hguogj487g84gkh");

Re-run your tests to make sure you haven’t broken something.

Third Test – Configuration

The next test is to start pulling configuration from a config file. As this article is quite large already, this will be covered in the next article.