When the Visual Studio Test Task in Azure DevOps Pipelines fails to find any tests it logs a warning and happily succeeds. It has been a regular request on the MVP community to do something about that and to ensure that builds fail when no tests have executed.

Since test results are published to the build results, it turned out to be quite easy to create a Server Task which handles this for you. This post will both introduce the task and explain how I built it.

Ensure Tests Task

You can find the Ensure Tests task in the Visual Studio Marketplace, once you have them installed you can add them to your build pipeline. To do so you first have to add an "Agentless job":

Add an agentless job

Then configure that to depend on all of the other phases in your build pipeline that are used to run tests:

Select all the phases that run tests to depend on

Add the "Ensure Tests" tasks to your Agentless job and you're all set!

Add "Ensure tests have executed" task to your Agentless phase.

How does it work?

The "Ensure tests have executed" task relies on the information that's automatically captured by Azure DevOps Pipelines when tests are executed. Test results and coverage data is automatically uploaded. This data is continuously available, even while the build is running.

The task queries the test result API and uses the same endpoint that's used by the Build Result page. I used the Chrome Developer Tools to look up the endpoint used by the build summary page to display the Total Tests number:

Use Chrome Developer Tools to find the correct request

The result json contains quite a few useful metrics, the ones I used was totalTests, but as you can see it would be easy to also check for the number of Test Runs, increases or decreases in the number of tests etc.

{
  "aggregatedResultsAnalysis": {
    "previousContext": {
      "contextType": 0,
      "build": null,
      "release": null
    },
    "resultsDifference": {
      "increaseInTotalTests": 16,
      "increaseInFailures": 0,
      "increaseInPassedTests": 16,
      "increaseInOtherTests": 0,
      "increaseInDuration": "00:00:02.5170000"
    },
    "totalTests": 16,                             // Number of tests
    "duration": "00:00:02.5170000",
    "resultsByOutcome": {
      "Passed": {
        "outcome": "passed",
        "count": 16,
        "duration": "00:00:02.1570000"
      }
    },
    "runSummaryByState": {
      "Completed": {
        "state": "completed",
        "runsCount": 1,                           // Number of test runs
        "resultsByOutcome": {
          "Passed": {
            "outcome": "passed",
            "count": 16,
            "duration": "00:00:02.1570000"
          }
        }
      }
    },
    "runSummaryByOutcome": {
      "Passed": {
        "runsCount": 1
      }
    }
  },
  "testFailures": {
    "previousContext": null,
    "newFailures": {
      "count": 0,
      "testResults": []
    },
    "existingFailures": {
      "count": 0,
      "testResults": []
    },
    "fixedTests": {
      "count": 0,
      "testResults": []
    }
  },
  "testResultsContext": {
    "contextType": "build",
    "build": {
      "id": 120,
      "definitionId": 0,
      "uri": "vstfs:///Build/Build/120"
    },
    "release": null
  },
  "teamProject": {
    "id": "068e8d2d-878b-405c-9d71-e653b8412284",
    "name": "PSD-001",
    "state": "unchanged",
    "visibility": "unchanged"
  }
}

I then used my Server Expression Tester to create the correct condition to pass or fail the build using the above information.

You can find more details on the Server Expression Tester in a previous post and download the latest version from GitHub.
Use the Server Expression Tester to validate the Condition Expression for the task using the captured json response.

And then proceeded wrapping that into a Task:

{
  "id": "25d3d29e-5ea1-4453-9ce6-02e1b34ab30c",
  "name": "Ensure tests have executed.",
  "friendlyName": "Ensure tests have executed.",
  "description": "Ensure tests have executed.",
  "author": "Jesse Houwing",
  "helpMarkDown": "",
  "category": "Test",
  "version": {
    "Major": 0,
    "Minor": 0,
    "Patch": 3
  },
  "visibility": [
    "Build",
    "Release"
  ],
  "runsOn": [
    "Server"
  ],
  "preview": true,
  "instanceNameFormat": "Ensure tests have executed",
  "inputs": [],
  "execution": {
    "HttpRequest": {
      "Execute": {
        "EndpointId": "",
        "EndpointUrl": "$(system.teamFoundationCollectionUri)$(System.TeamProject)/_apis/test/ResultSummaryByBuild?buildId=$(Build.BuildId)",
        "Method": "GET",
        "Body": "",
        "Headers":"{\n\"Content-Type\":\"application/json\"\n, \"Authorization\":\"Bearer $(system.accesstoken)\"\n}",
        "WaitForCompletion": "false",
        "Expression": "le(1, jsonpath('$.aggregatedResultsAnalysis.totalTests')[0])"
      }
    }
  }
}

I grabbed the EndPointUrl from the QueryWorkItems task on the vsts-tasks GitHub tasks.

Tip: Many of Microsoft's tasks do fancy things. Some of these tricks aren't well documented, but most of it is happening out in the open. If you want to replicate a certain behavior it pays to browse through one of these repositories:

- https://github.com/Microsoft/vsts-tasks/
- https://github.com/Microsoft/vsts-task-lib
- https://github.com/Microsoft/vsts-agent

Not only will you find examples and test approaches, but also preliminary documentation.

I packaged the task up in an extension and published it using the CI/CD Tools for Azure DevOps Extensions like I do with all of my extensions.

It looks like people are finding this a useful extension, there have been 21 installs, only 5 star reviews and I've already merged the first pull request. What other test metrics would you like to Ensure as part of your build pipeline? Leave a comment below or file an issue on GitHub!

Photo by: Jon Ardern used under Creative Commons