fbpx

When setting up CI/CD (Continuous Integration and Continuous Deployment) pipelines in Azure DevOps for .NET 7+ solutions, such as those using ASP.NET Core, it’s important to setup an automated build. The automated build will need to use the dotnet CLI to build the application, and run dotnet test to execute the unit tests within the solution. These are the most common tasks configured within Azure DevOps Pipelines when setting up automated CI/CD workflows for .NET applications. The next items that are extremely valuable for monitoring the build is to integrate the Unit Test results and the Code Coverage results for those tests within the Azure Pipeline. This article will show you how to configure the YAML tasks for setting up Unit Test and Code Coverage integration within an Azure DevOps Pipeline.

Let’s do this!

Basic Azure Pipeline YAML to Build .NET 7 Solution and Run Unit Tests

For this example, we’ll write an Azure Pipeline in YAML format that targets building and running the unit tests for a .NET 7 application written as a Visual Studio 2022 solution (.sln file and related files) that is contained within a git Azure Repository.

The following is the basic YAML code needed to configure an Azure Pipeline for building this .NET 7 solution and running the Unit Tests:

# Automatically trigger on merges / commits to 'main' branch
trigger:
- main

# Specify build agent image
pool:
  vmImage: 'windows-latest'

# Some config variables for pipeline
variables:
  # specify the Build Configuration to build for the solution
  buildConfiguration: 'debug' #'Release'

stages:
- stage: 'Build'
  displayName: 'Build the web application'
  jobs:
  - job: 'Build'
    displayName: 'Build job'

    steps:
    - task: NuGetToolInstaller@1

    - task: NuGetCommand@2
      inputs:
        restoreSolution: '**/*.sln'

    - task: UseDotNet@2
      displayName: 'Use .NET SDK v7.x'
      inputs:
        version: '7.x'

    - task: DotNetCoreCLI@2
      displayName: 'Restore project dependencies'
      inputs:
        command: 'restore'
        projects: '**/*.csproj'

    - task: DotNetCoreCLI@2
      displayName: 'Build the project - $(buildConfiguration)'
      inputs:
        command: 'build'
        arguments: '--no-restore --configuration $(buildConfiguration)'
        projects: '**/*.csproj'

    - task: DotNetCoreCLI@2
      displayName: 'Install .NET tools from local manifest'
      inputs:
        command: custom
        custom: tool
        arguments: 'restore'

    - task: DotNetCoreCLI@2
      displayName: 'Run Unit Tests - $(buildConfiguration)'
      inputs:
        command: 'test'
        arguments: '--no-build --configuration $(buildConfiguration)'
        publishTestResults: false

    - task: DotNetCoreCLI@2
      displayName: 'Publish the project - $(buildConfiguration)'
      inputs:
        command: 'publish'
        projects: '**/*.csproj'
        publishWebProjects: false
        arguments: '--no-build --configuration $(buildConfiguration) --output $(Build.ArtifactStagingDirectory)/$(buildConfiguration)'
        zipAfterPublish: true

    - task: PublishBuildArtifacts@1
      displayName: 'Publish Artifact: drop'
      condition: succeeded()

This Azure Pipeline YAMl is configured to use the windows-latest image in Azure DevOps for the Build Agent Pool. This is a managed image available from Microsoft in Azure DevOps. In most enterprise scenarios, you will host your own build agents. These build agents may even be configured with Linux instead. Whether you’re using a Windows or Linux build agent image, the YAMl pipeline configuration in this article should work just the same.

For clarity, the tasks configured in this Azure Pipeline YAML are as follows:

  1. NuGetToolInstaller@1 – This task is configured to ensure that Nuget is installed on the Build Agent.
  2. NuGetCommand@2 – Call Nuget to restore dependencies for the solution.
  3. UseDotNet@2 – This task is configured to ensure that the .NET 7.x SDK is installed on the Build Agent.
  4. DotNetCoreCLI@2 – The dotnet restore command to restore dependencies.
  5. DotNetCoreCLI@2 – The dotnet build command to build the solution. This also specifies the --configuration argument to pass in the Build Configuration to build; in this case it’s configured for the debug configuration using the $(buildConfiguration) variable defined in the pipeline.
  6. DotNetCoreCLI@2 – The dotnet tool restore command to install .NET tools from the local manifest in the repository.
  7. DotNetCoreCLI@2 – Run the Unit Tests using the dotnet test command. This also uses the --configuration argument to pass in the Build Configuration to target.
  8. DotNetCoreCLI@2 – The dotnet publish command to publish the built projects as .zip archives and save them to the artifact staging directory $(Build.ArtifactStagingDirectory) for the pipeline.
  9. PublishBuildArtifacts@1 – Perform a Build Artifact Publish to the pipeline of the project .zip files that were published in the previous task. This is so the build results of the pipeline are available later within the artifacts for the pipeline.

This is a great basic Azure Pipeline for a .NET 7 solution that will get you started configuring CI/CD for the solution. There are obviously custom configurations that may be needed, as well as publishing and deploying the app to your environment. The pipeline includes a publish step that publishes the built app to Artifacts on the Pipeline. For now, we’ll leave the remaining parts out as they are outside the scope of this article.

Next we’ll take a look at the primary components of this article that are integrating the Unit Test Results and Code Coverage Results to display in the Azure DevOps Pipeline. This will help you more easily manage and monitor the build.

Run Unit Tests with VSTest and Output Unit Test and Code Coverage Results Files

There are a couple different ways to run the Unit Tests for the .NET solution within the Azure Pipeline. The method chosen in this article using the DotNetCoreCLI@2 task to run the dotnet test command with some arguments passed in. Let’s take a look at modifying this task to also output the Unit Test results and Code Coverage results!

The following is the basic task using DotNetCoreCLI@2 to run the dotnet test command for executing the Unit Tests within the solution:

    - task: DotNetCoreCLI@2
      displayName: 'Run Unit Tests - $(buildConfiguration)'
      inputs:
        command: 'test'
        arguments: '--no-build --configuration $(buildConfiguration)'
        publishTestResults: false

This task is passing in the following 2 arguments to the dotnet test command:

  • --no-build – This tells it to skip building the solution. We’ll do this since it’s already been done previously in the pipeline.
  • --configuration – This is used to pass in the Build Configuration for the Visual Studio solution. In this example, the $(buildConfiguration) variable within the pipeline is being used to configure the Build Configuration to use; which is configured to debug.

Yes, the DotNetCLI@2 task has the publishTestResults input that can be configured. However, when this is done, the task will not allow both Unit Test results and Code Coverage results to be published. So for now, we’ll set the publishTestResults input to false, so we can override this with our own configuration of arguments to output the results as needed.

To configure the dotnet test command to output both Unit Test and Code Coverage results, a couple more arguments are needed for each.

Configure dotnet test to Output Unit Test Results

The DotNetCoreCLI@2 task for running the Unit Tests (via dotnet test command) requires additional arguments to configure it to output the Unit Test Results. The argument to use is the --logger argument and will be used to configure the unit test results format to output. In this case, for VSTest format, the trx value will be configured.

The following is the --logger argument that’s required for this:

--logger trx

Next, the --results-directory argument is required to tell the task to output the unit test results files to the desired directory. In this article, we’ll build the pipeline to use the $(Build.SourceDirectory)/TestResults/Coverage/ directory for this.

The following is the --results-directory argument that is required for this:

--results-directory "$(Build.SourcesDirectory)/TestResults/Coverage/"

The following is the task to run the Unit Tests with these required arguments for it to output the VSTest .trx results files in the desired directory:

    - task: DotNetCoreCLI@2
      displayName: 'Run Unit Tests - $(buildConfiguration)'
      inputs:
        command: 'test'
        arguments: '--no-build --configuration $(buildConfiguration) --logger trx --results-directory "$(Build.SourcesDirectory)/TestResults/Coverage/"'
        publishTestResults: false

Configure dotnet test to Output Code Coverage Results

he DotNetCoreCLI@2 task for running the Unit Tests (via dotnet test command) requires additional arguments to configure it to output the Code Coverage Results. The argument to use is the --collect argument and will be used to configure the code coverage data collector to use. In this case, to use Coverlet, the value of XPlat Code Coverage will be configured.

The following is the --collect argument that’s required for this:

--collect "XPlat Code Coverage"

Next, the --results-directory argument is required to tell the task to output the unit test results files to the desired directory. In this article, we’ll build the pipeline to use the $(Build.SourceDirectory)/TestResults/Coverage/ directory for this.

The following is the --results-directory argument that is required for this:

--results-directory "$(Build.SourcesDirectory)/TestResults/Coverage/"

The following is the task to run the Unit Tests with these required arguments for it to output the Coverlet Code Coverage Results files in the desired directory:

    - task: DotNetCoreCLI@2
      displayName: 'Run Unit Tests - $(buildConfiguration)'
      inputs:
        command: 'test'
        arguments: '--no-build --configuration $(buildConfiguration) --results-directory "$(Build.SourcesDirectory)/TestResults/Coverage/" --collect "XPlat Code Coverage"'
        publishTestResults: false

Full DotNetCoreCLI@2 Task to Output Unit Test and Code Coverage Results

Now, let’s put the two examples of configuring the DotNetCoreCLI@2 task running the dotnet test command together so we have a single YAML task that will output both the VSTest Unit Test Results and Coverlet Code Coverage Results.

The following is the full DotNetCoreCLI@2 task is configured to output both the unit test and code coverage results:

    - task: DotNetCoreCLI@2
      displayName: 'Run Unit Tests - $(buildConfiguration)'
      inputs:
        command: 'test'
        arguments: '--no-build --configuration $(buildConfiguration) --logger trx --results-directory "$(Build.SourcesDirectory)/TestResults/Coverage/"  --collect "XPlat Code Coverage"'
        publishTestResults: false

To use this in the Azure Pipeline YAML example shown previously, simply replace the Unit Test task within the previous Azure Pipeline YAML with this code. This will configure the pipeline to output the unit test and code coverage results. The next steps are to add tasks to the YAML that will publish these results to the Azure Pipeline.

Publish VSTest Unit Test Result to Azure Pipeline

Using the previous DotNetCoreCLI@2 task that runs dotnet test and outputs the VSTest .trx format Unit Test Results, the next step is to publish the unit test results to the Azure Pipeline.

Since the task that ran the Unit Tests using dotnet test previously was configured to output the test results in .trx format, then the PublishTestResults@2 task can be configured as follows:

    - task: PublishTestResults@2
      displayName: 'Publish Test Results'
      inputs:
        testResultsFormat: VSTest
        testResultsFiles: '**/*.trx'
        searchFolder: '$(Build.SourcesDirectory)/TestResults/Coverage/'

The following inputs on the task are configured as follows:

  • testResultsFormat – Configured to VSTest since VSTest was used by dotnet test to run the unit tests and the results are in .trx format.
  • testResultsFiles – Configured to the filter of **/*.trx so it look for .trx files to publish the unit test results from.
  • searchFolder – This is configured to the directory that the unit test results were output to. For the examples in this article it’s $(Build.SourcesDirectory)/TestResults/Coverage/.

To use this task, it must be places after the Run Unit Tests task in the Azure Pipeline YAML.

Publish Coverlet and Cobertura Code Coverage Results to Azure Pipeline

Using the previous DotNetCoreCLI@2 task that runs dotnet test and outputs the VSTest .trx format Unit Test Results, the next step is to publish the Code Coverage Results. The task is configured to output the code coverage results using Coverlet. Before these results can be published to the Azure Pipeline, the process chosen in this article requires a Code Coverage Report to be generated. To generate this report, we can use reportgenerator.

The tasks required for this process to generate the code coverage report and publish it to the Azure Pipeline are as follows:

  1. DotNetCoreCLI@2 – The dotnet tool install command will be used to install the reportgenerator tool.
  2. PowerShell@2 – The reportgenerator tool will be called to generate the Code Coverage Report, using Cobertura, based on the Coverlet format Code Coverage Results that were previously output from the running the Unit Tests.
  3. PublishCodeCoverageResults@1 – Publish the Code Coverage Report that was generated using reportgenerator to the Azure Pipeline.

The following YAML are these 3 tasks configured as appropriate:

    - task: DotNetCoreCLI@2
      displayName: 'dotnet Tool Install "reportgenerator"'
      inputs:
        command: custom
        custom: tool
        arguments: 'install -g dotnet-reportgenerator-globaltool'

    - task: PowerShell@2
      displayName: 'Create Code Coverage Report'
      inputs:
        targetType: 'inline'
        script: reportgenerator -reports:$(Build.SourcesDirectory)/**/coverage.cobertura.xml -targetdir:$(Build.SourcesDirectory)/CodeCoverage -reporttypes:HtmlInline_AzurePipelines

    - task: PublishCodeCoverageResults@1
      displayName: 'Publish Code Coverage Report'
      inputs:
        codeCoverageTool: 'cobertura'
        summaryFileLocation: '$(Build.SourcesDirectory)/**/coverage.cobertura.xml'

Keep in mind, this YAML code is also configured for the reportgenerator to look in the $(Build.SourcesDirectory)/CodeCoverage directory using the targetdir argument for the Code Coverage Results that were output from the dotnet test command that ran the Unit Tests previously.

To use this task, it must be places after the Run Unit Tests task in the Azure Pipeline YAML.

Azure Pipeline Display of Unit Test and Code Coverage Results

Once the Unit Test results and the Code Coverage results publishing has been integrated into the YAML pipeline, when the pipeline is executed there will be 2 new tabs added to the build results for the pipeline status. The overall Unit Test results pass/fail percentages and Code Coverage percentages will also be displayed on the main Summary view for the pipeline build results.

The following is a screenshot of the pipeline Summary view with these results shown:

Azure Pipeline: Publish Unit Test and Code Coverage Results with .NET 7 Solution using VSTest, Cobertura, and Coverlet 4
Azure Pipeline showing Unit Test and Code Coverage results

The following tabs will be added:

  • Tests – This tab will display the Unit Test results for the solution as published by the YAML pipeline.
  • Code Coverage – This will display the Code Coverage Report that was generated and published by the YAML pipeline.

The following is a screenshot of the Tests tab for the Azure Pipeline showing the full Unit Test results that also allows you to drill into view the full unit test pass/fail results:

Azure Pipeline: Publish Unit Test and Code Coverage Results with .NET 7 Solution using VSTest, Cobertura, and Coverlet 5
Azure Pipeline Tests tab showing Unit Test results

The following is a screenshot of the Code Coverage tab for the Azure Pipeline showing the full Code Coverage Report and results that also allows you to drill into view the full code coverage details:

Azure Pipeline: Publish Unit Test and Code Coverage Results with .NET 7 Solution using VSTest, Cobertura, and Coverlet 6
Azure Pipeline Code Coverage tab showing Code Coverage report

Full Azure DevOps .NET 7 CI/CD Pipeline YAML with Unit Test and Code Coverage Integration

The following is a full example of the various YAML tasks discussed in this article put together as a full Azure Pipeline. The task for running the unit tests has been modified appropriately, and the Unit Test Publish and Code Coverage Publish tasks have be placed immediately before the “Publish the project” task.

# Automatically trigger on merges / commits to 'main' branch
trigger:
- main

# Specify build agent image
pool:
  vmImage: 'windows-latest'

# Some config variables for pipeline
variables:
  # specify the Build Configuration to build for the solution
  buildConfiguration: 'debug' #'Release'

stages:
- stage: 'Build'
  displayName: 'Build the web application'
  jobs:
  - job: 'Build'
    displayName: 'Build job'

    steps:
    - task: NuGetToolInstaller@1

    - task: NuGetCommand@2
      inputs:
        restoreSolution: '**/*.sln'

    - task: UseDotNet@2
      displayName: 'Use .NET SDK v7.x'
      inputs:
        version: '7.x'

    - task: DotNetCoreCLI@2
      displayName: 'Restore project dependencies'
      inputs:
        command: 'restore'
        projects: '**/*.csproj'

    - task: DotNetCoreCLI@2
      displayName: 'Build the project - $(buildConfiguration)'
      inputs:
        command: 'build'
        arguments: '--no-restore --configuration $(buildConfiguration)'
        projects: '**/*.csproj'

    - task: DotNetCoreCLI@2
      displayName: 'Install .NET tools from local manifest'
      inputs:
        command: custom
        custom: tool
        arguments: 'restore'

    # run unit tests and generate both:
    # 1. Test results in .trx file format / VSTest format
    # 2. Code coverage results in Cobertura file format
    - task: DotNetCoreCLI@2
      displayName: 'Run Unit Tests - $(buildConfiguration)'
      inputs:
        command: 'test'
        arguments: '--no-build --configuration $(buildConfiguration) --logger trx --results-directory "$(Build.SourcesDirectory)/TestResults/Coverage/"  --collect "XPlat Code Coverage"'
        publishTestResults: false

    - task: PublishTestResults@2
      displayName: 'Publish Test Results'
      inputs:
        testResultsFormat: VSTest
        testResultsFiles: '**/*.trx'
        searchFolder: '$(Build.SourcesDirectory)/TestResults/Coverage/'

    - task: DotNetCoreCLI@2
      displayName: 'dotnet Tool Install "reportgenerator"'
      inputs:
        command: custom
        custom: tool
        arguments: 'install -g dotnet-reportgenerator-globaltool'

    - task: PowerShell@2
      displayName: 'Create Code Coverage Report'
      inputs:
        targetType: 'inline'
        script: reportgenerator -reports:$(Build.SourcesDirectory)/**/coverage.cobertura.xml -targetdir:$(Build.SourcesDirectory)/CodeCoverage -reporttypes:HtmlInline_AzurePipelines

    - task: PublishCodeCoverageResults@1
      displayName: 'Publish Code Coverage Report'
      inputs:
        codeCoverageTool: 'cobertura'
        summaryFileLocation: '$(Build.SourcesDirectory)/**/coverage.cobertura.xml'

    - task: DotNetCoreCLI@2
      displayName: 'Publish the project - $(buildConfiguration)'
      inputs:
        command: 'publish'
        projects: '**/*.csproj'
        publishWebProjects: false
        arguments: '--no-build --configuration $(buildConfiguration) --output $(Build.ArtifactStagingDirectory)/$(buildConfiguration)'
        zipAfterPublish: true

    - task: PublishBuildArtifacts@1
      displayName: 'Publish Artifact: drop'
      condition: succeeded()

Conclusion

I recently had the opportunity to configure a .NET solution build pipeline in Azure DevOps that need to integrate the Unit Test and Code Coverage results into the Azure Pipeline. This was a little tricky to figure out as it seems things changed a little with this in regards to .NET 7+ as compared to previously with .NET 6 and earlier. At least that was the impression that I got when looking up the steps and troubleshooting errors. So, I thought I’d write this article up to help both my future self and anyone else needing to do this.

Sure this may not be the best way to configure an YAML-based Azure Pipeline to publish the Unit Test and Code Coverage Results using VSTest, Coverlet, and Cobertura. There likely a couple other possible solutions, but this is the solution that I found worked for my project, so I thought I’d share it.

I hope this helped you, and happy automating those CI/CD pipelines in Azure DevOps!

Microsoft MVP

Chris Pietschmann is a Microsoft MVP, HashiCorp Ambassador, and Microsoft Certified Trainer (MCT) with 20+ years of experience designing and building Cloud & Enterprise systems. He has worked with companies of all sizes from startups to large enterprises. He has a passion for technology and sharing what he learns with others to help enable them to learn faster and be more productive.
HashiCorp Ambassador Microsoft Certified Trainer (MCT) Microsoft Certified: Azure Solutions Architect

Discover more from Build5Nines

Subscribe now to keep reading and get access to the full archive.

Continue reading