---
title: "Upload test results"
description: "Upload test results with the store_test_results step to see them in CircleCI. Includes JUnit XML formatter setup and test runner examples by language."
platform: "Cloud, Server"
doc_version: "Server"
last_updated: "2026-10-27"
---

> For current CircleCI product defaults, deprecated patterns, and Cloud/Server differences, see [AGENTS.md](https://circleci.com/docs/AGENTS.md).
>
> For the complete documentation index and site structure, see [llms.txt](https://circleci.com/docs/llms.txt).

# Upload test results

Upload your test results to CircleCI with the [`store_test_results` step](https://circleci.com/docs/reference/configuration-reference/#storetestresults). CircleCI reads the JUnit XML files that your test runner writes, and uses them to show your test results and power its testing features. This page describes how to configure common test runners to output JUnit XML, and how to upload the results.

Uploading test results gives you access to:

*   The **Tests** tab in the CircleCI web app, which shows the passing and failing tests for each job.
    
*   Timing data for test splitting.
    

> **Image:** store-test-results-view

Figure 1. Test summary view

## Configure test uploads

Add a `store_test_results` step to your job in `.circleci/config.yml`, after the step that runs your tests:

```yml
steps:
  - run:
  #...
  # run tests and store XML files to a subdirectory, for example, test-results
  #...
  - store_test_results:
      path: test-results
```

The `path` key is an absolute or relative path to your `working_directory`. It can contain JUnit XML test metadata files directly in the directory, in subdirectories, or point to a single file containing all test results.

Make sure that your `path` value is not a hidden folder. For example, `.my_hidden_directory` would be an invalid format.

To look at the raw XML files, for example when debugging your test uploads, you can also store test results as artifacts with the `store_artifacts` step. See the [Storing Build Artifacts](https://circleci.com/docs/guides/server/optimize/artifacts/) page.

## Enabling formatters

Test metadata is not automatically collected in CircleCI until you enable the JUnit formatters. For RSpec, minitest, and Django, add the following configuration to enable the formatters:

*   RSpec requires the following be added to your Gemfile:
    
    ```ruby
    gem 'rspec_junit_formatter'
    ```
    
*   minitest requires the following be added to your Gemfile:
    
    ```ruby
    gem 'minitest-ci'
    ```
    
*   Configure Django using the [`django-nose`](https://github.com/django-nose/django-nose) test runner.
    

## Test runner examples by language

This section provides the following test runner examples:

| Language | Test Runner | Formatter | Examples |
| --- | --- | --- | --- |
| JavaScript | Jest | [`jest-junit`](https://www.npmjs.com/package/jest-junit) | [example](#jest) |
| JavaScript | Vitest | built-in | [example](#vitest) |
| JavaScript | Mocha | [`mocha-junit-reporter`](https://www.npmjs.com/package/mocha-junit-reporter) | [example](#mocha-for-node), [example with nyc](#mocha-with-nyc) |
| JavaScript | Karma | [`karma-junit-reporter`](https://www.npmjs.com/package/karma-junit-reporter) | [example](#karma) |
| JavaScript | Ava | [tap-xunit](https://github.com/aghassemi/tap-xunit) | [example](#ava-for-node) |
| JavaScript | ESLint | [JUnit formatter](https://eslint.org/docs/user-guide/formatters/#junit) | [example](#eslint) |
| JavaScript | Playwright | built-in | [example](#playwright) |
| JavaScript | Cypress | [`mocha-junit-reporter`](https://www.npmjs.com/package/mocha-junit-reporter) | [example](#cypress) |
| JavaScript | WebdriverIO | [`@wdio/junit-reporter`](https://www.npmjs.com/package/@wdio/junit-reporter) | [example](#webdriverio) |
| Ruby | RSpec | [`rspec_junit_formatter`](https://rubygems.org/gems/rspec_junit_formatter) | [example](#rspec) |
| Ruby | minitest | [minitest-ci](https://rubygems.org/gems/minitest-ci) | [example](#minitest) |
| — | Cucumber | built-in | [example](#cucumber) |
| Python | pytest | built-in | [example](#pytest) |
| Python | unittest | Use [pytest](https://docs.pytest.org/en/6.2.x/unittest.html) to run these tests | [example](#unittest) |
| Java | Maven | [Maven Surefire plugin](https://maven.apache.org/surefire/maven-surefire-plugin/) | [example](#maven-surefire-plugin-for-java-junit-results) |
| Java | Gradle | built-in | [example](#gradle-junit-test-results) |
| PHP | PHPUnit | built-in | [example](#phpunit) |
| .NET | — | [trx2junit](https://github.com/gfoidl/trx2junit) | [example](#trx2junit-for-visual-studio-net-core-tests) |
| .NET | NUnit | [`nunit-transforms`](https://github.com/nunit/nunit-transforms/tree/master/nunit3-junit) | [example](https://github.com/nunit/nunit-transforms/tree/master/nunit3-junit) |
| Clojure | Kaocha | [`kaocha-junit-xml`](https://clojars.org/lambdaisland/kaocha-junit-xml) | [example](#kaocha) |
| Clojure | clojure.test | [test2junit](https://github.com/ruedigergad/test2junit) | [example](#test2junit-for-clojure-tests) |
| C, C++ | CTest | [CTest](https://cmake.org/cmake/help/latest/manual/ctest.1.html#cmdoption-ctest-output-junit) | [example](#ctest-for-c-cxx-tests) |
| C++ | GoogleTest | built-in | [example](#googletest) |
| Go | go test | [gotestsum](https://github.com/gotestyourself/gotestsum) | [example](#go) |
| Bash | Bats | built-in | [example](#bats-for-bash) |

### Jest

To output JUnit compatible test data with Jest you can use [`jest-junit`](https://www.npmjs.com/package/jest-junit).

A working `.circleci/config.yml` section might look like this:

```yml
    steps:
      - run:
          name: Install JUnit coverage reporter
          command: yarn add --dev jest-junit
      - run:
          name: Run tests with JUnit as reporter
          command: jest --ci --runInBand --reporters=default --reporters=jest-junit
          environment:
            JEST_JUNIT_OUTPUT_DIR: ./reports/
            JEST_JUNIT_ADD_FILE_ATTRIBUTE: "true"
      - store_test_results:
          path: ./reports/
```

For a full walkthrough, refer to this article [Using JUnit on CircleCI 2.0 with Jest and ESLint](https://www.viget.com/articles/using-junit-on-circleci-2-0-with-jest-and-eslint) by Viget. Note that usage of the Jest CLI argument `--testResultsProcessor` in the article has been superseded by the `--reporters` syntax, and JEST\_JUNIT\_OUTPUT has been replaced with `JEST_JUNIT_OUTPUT_DIR` and `JEST_JUNIT_OUTPUT_NAME`, as demonstrated above.

When running Jest tests, use the `--runInBand` flag. Without this flag, Jest will try to allocate the CPU resources of the entire virtual machine in which your job is running. Using `--runInBand` will force Jest to use only the virtualized build environment within the virtual machine.

For more details on `--runInBand`, refer to the [Jest CLI](https://jestjs.io/docs/cli#--runinband) documentation. For more information on these issues, see [Issue 1524](https://github.com/jestjs/jest/issues/1524#issuecomment-262366820) and [Issue 5239](https://github.com/jestjs/jest/issues/5239#issuecomment-355867359) of the official Jest repository.

### Vitest

Vitest has a built-in JUnit reporter. Set the reporter and output file on the command line:

```yml
    steps:
      - run:
          name: Run tests with JUnit as reporter
          command: npx vitest run --reporter=default --reporter=junit --outputFile.junit=test-results/junit.xml
      - store_test_results:
          path: test-results
```

### Mocha for Node.js

To output JUnit tests with the Mocha test runner you can use [`mocha-junit-reporter`](https://www.npmjs.com/package/mocha-junit-reporter).

A working `.circleci/config.yml` section for testing might look like this:

```yml
    steps:
      - checkout
      - run: npm install
      - run: mkdir ~/junit
      - run:
          command: mocha test --reporter mocha-junit-reporter
          environment:
            MOCHA_FILE: ~/junit/test-results.xml
          when: always
      - store_test_results:
          path: ~/junit
```

### Mocha with nyc

Following is an example for Mocha with nyc, contributed by [`marcospgp`](https://github.com/marcospgp).

```yml
version: '2.1'

jobs:
  build:
    environment:
      CC_TEST_REPORTER_ID: code_climate_id_here
      NODE_ENV: development
    docker:
      - image: cimg/node:16.10
        environment:
          MONGODB_URI: mongodb://admin:password@localhost:27017/db?authSource=admin
      - image: mongo:4.0
        environment:
          MONGO_INITDB_ROOT_USERNAME: admin
          MONGO_INITDB_ROOT_PASSWORD: password
    working_directory: ~/repo
    steps:
      - checkout

      # Update npm
      - run:
          name: update-npm
          command: 'sudo npm install -g npm@latest'

      # Download and cache dependencies
      - restore_cache:
          keys:
              - v1-dependencies-{{ checksum "package-lock.json" }}
              # fallback to using the latest cache if no exact match is found
              - v1-dependencies-

      - run: npm install

      - run: npm install mocha-junit-reporter # just for CircleCI

      - save_cache:
          paths:
              - node_modules
          key: v1-dependencies-{{ checksum "package-lock.json" }}

      - run: mkdir reports

      # Run mocha
      - run:
          name: npm test
          command: ./node_modules/.bin/nyc ./node_modules/.bin/mocha --recursive --timeout=10000 --exit --reporter mocha-junit-reporter --reporter-options mochaFile=reports/mocha/test-results.xml
          when: always

      # Run eslint
      - run:
          name: eslint
          command: |
              ./node_modules/.bin/eslint ./ --format junit --output-file ./reports/eslint/eslint.xml
          when: always

      # Run coverage report for Code Climate

      - run:
          name: Setup Code Climate test-reporter
          command: |
              # download test reporter as a static binary
              curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./cc-test-reporter
              chmod +x ./cc-test-reporter
              ./cc-test-reporter before-build
          when: always

      - run:
          name: code-coverage
          command: |
              mkdir coverage
              # nyc report requires that nyc has already been run,
              # which creates the .nyc_output folder containing necessary data
              ./node_modules/.bin/nyc report --reporter=text-lcov > coverage/lcov.info
              ./cc-test-reporter after-build -t lcov
          when: always

      # Upload results

      - store_test_results:
          path: reports

      - store_artifacts: # upload test coverage as artifact
          path: ./coverage/lcov.info
          prefix: tests
```

### Karma

To output JUnit tests with the Karma test runner you can use [`karma-junit-reporter`](https://www.npmjs.com/package/karma-junit-reporter).

A working `.circleci/config.yml` section might look like this:

```yml
    steps:
      - checkout
      - run: npm install
      - run: mkdir ~/junit
      - run:
          command: karma start ./karma.conf.js
          environment:
            JUNIT_REPORT_PATH: ./junit/
            JUNIT_REPORT_NAME: test-results.xml
          when: always
      - store_test_results:
          path: ./junit
```

```javascript
// karma.conf.js

// additional config...
{
  reporters: ['junit'],
  junitReporter: {
    outputDir: process.env.JUNIT_REPORT_PATH,
    outputFile: process.env.JUNIT_REPORT_NAME,
    useBrowserName: false
  },
}
// additional config...
```

### Ava for Node.js

To output JUnit tests with the [Ava](https://github.com/avajs/ava) test runner you can use the TAP reporter with [tap-xunit](https://github.com/aghassemi/tap-xunit).

A working `.circleci/config.yml` section for testing might look like the following example:

```yml
    steps:
      - run:
          command: |
            yarn add ava tap-xunit --dev # or you could use npm
            mkdir -p ~/reports
            ava --tap | tap-xunit > ~/reports/ava.xml
          when: always
      - store_test_results:
          path: ~/reports
```

### ESLint

To output JUnit results from [ESLint](https://eslint.org/), you can use the [JUnit formatter](https://eslint.org/docs/user-guide/formatters/#junit).

A working `.circleci/config.yml` test section might look like this:

```yml
    steps:
      - run:
          command: |
            mkdir -p ~/reports
            eslint ./src/ --format junit --output-file ~/reports/eslint.xml
          when: always
      - store_test_results:
          path: ~/reports
```

### RSpec

To add test metadata collection to a project that uses a custom `rspec` build step, add the following gem to your Gemfile:

```ruby
gem 'rspec_junit_formatter'
```

And modify your test command to this:

```yml
    steps:
      - checkout
      - run: bundle check --path=vendor/bundle || bundle install --path=vendor/bundle --jobs=4 --retry=3
      - run: mkdir ~/rspec
      - run:
          command: bundle exec rspec --format progress --format RspecJunitFormatter -o ~/rspec/rspec.xml
          when: always
      - store_test_results:
          path: ~/rspec
```

### minitest

To add test metadata collection to a project that uses a custom `minitest` build step, add the following gem to your Gemfile:

```ruby
gem 'minitest-ci'
```

And modify your test command to this:

```yml
    steps:
      - checkout
      - run: bundle check || bundle install
      - run:
          command: bundle exec rake test
          when: always
      - store_test_results:
          path: test/reports
```

See the [minitest-ci README](https://github.com/circleci/minitest-ci#readme) for more info.

### Cucumber

For custom Cucumber steps, generate a file using the JUnit formatter and write it to the `cucumber` directory. The following is an example of the addition to your `.circleci/config.yml` file:

```yml
    steps:
      - run:
          name: Save test results
          command: |
            mkdir -p ~/cucumber
            bundle exec cucumber --format junit --out ~/cucumber/junit.xml
          when: always
      - store_test_results:
          path: ~/cucumber
```

The `path:` is a directory relative to the project’s root directory where the files are stored. CircleCI collects and uploads the artifacts to S3 and makes them available in the **Artifacts tab** of the **Job page** in the application.

### pytest

To add test metadata to a project that uses `pytest` you need to tell it to output JUnit XML, and then save the test metadata:

```yml
    steps:
      - run:
          name: run tests
          command: |
            . venv/bin/activate
            mkdir test-results
            pytest --junitxml=test-results/junit.xml

      - store_test_results:
          path: test-results
```

### unittest

unittest does not support JUnit XML, but in almost all cases you can [run unittest tests with pytest](https://docs.pytest.org/en/6.2.x/unittest.html).

After adding pytest to your project, you can produce and upload the test results like this:

```yml
    steps:
      - run:
          name: run tests
          command: |
            . venv/bin/activate
            mkdir test-results
            pytest --junitxml=test-results/junit.xml tests

      - store_test_results:
          path: test-results
```

### Maven Surefire Plugin for Java JUnit Results

If you are building a [Maven](https://maven.apache.org/) based project, you are more than likely using the [Maven Surefire plugin](https://maven.apache.org/surefire/maven-surefire-plugin/) to generate test reports in XML format. Add the following to the `.circleci/config.yml` file in your project to collect reports.

```yml
    steps:
      - run:
          name: Save test results
          command: |
            mkdir -p ~/test-results/junit/
            find . -type f -regex ".*/target/surefire-reports/.*xml" -exec cp {} ~/test-results/junit/ \;
          when: always
      - store_test_results:
          path: ~/test-results
```

### Gradle JUnit test results

If you are building a Java or Groovy based project with [Gradle](https://gradle.org/), test reports are automatically generated in XML format. Add the following to the `.circleci/config.yml` file in your project to collect reports.

```yml
    steps:
      - run:
          name: Save test results
          command: |
            mkdir -p ~/test-results/junit/
            find . -type f -regex ".*/build/test-results/.*xml" -exec cp {} ~/test-results/junit/ \;
          when: always
      - store_test_results:
          path: ~/test-results
```

### PHPUnit

For PHPUnit tests, generate a file using the `--log-junit` command line option and write it to the `/phpunit` directory. Your `.circleci/config.yml` might be:

```yml
    steps:
      - run:
          command: |
            mkdir -p ~/phpunit
            phpunit --log-junit ~/phpunit/junit.xml tests
          when: always
      - store_test_results:
          path: ~/phpunit
```

### trx2junit for Visual Studio / .NET core tests

Use [trx2junit](https://github.com/gfoidl/trx2junit) to convert Visual Studio / .NET Core `trx` output to XML format.

A working `.circleci/config.yml` section might look like this:

```yml
    steps:
      - checkout
      - run: dotnet build
      - run: dotnet test --no-build --logger "trx"
      - run:
          name: test results
          when: always
          command: |
              dotnet tool install -g trx2junit
              export PATH="$PATH:/root/.dotnet/tools"
              trx2junit tests/**/TestResults/*.trx
      - store_test_results:
          path: tests/TestResults
```

### Kaocha

Assuming that you are already using kaocha as your test runner, do the following to produce and store test results:

Add the `kaocha-junit-xml` plugin to your dependencies

Edit your `project.clj` to add the `lambdaisland/kaocha-junit-xml` plugin, or do the equivalent if you are using deps.edn.

```clojure
(defproject ,,,
  :profiles {,,,
             :dev {:dependencies [,,,
                                  [lambdaisland/kaocha-junit-xml "0.0.76"]]}})
```

Edit the kaocha config file `test.edn` to use this test reporter

```clojure
#kaocha/v1
{:plugins [:kaocha.plugin/junit-xml]
 :kaocha.plugin.junit-xml/target-file "junit.xml"}
```

Add the `store_test_results` step your `.circleci/config.yml`

```yml
version: 2.1
jobs:
  build:
    docker:
      - image: circleci/clojure:tools-deps-1.9.0.394
    steps:
      - checkout
      - run: bin/kaocha
      - store_test_results:
          path: junit.xml
```

### test2junit for Clojure tests

Use [test2junit](https://github.com/ruedigergad/test2junit) to convert Clojure test output to XML format. For more details, refer to the [sample project](https://github.com/kimh/circleci-build-recipies/tree/clojure-test-metadata-with-test2junit).

### CTest for C/C++ Tests

CTest provides a [`--output-junit`](https://cmake.org/cmake/help/latest/manual/ctest.1.html#cmdoption-ctest-output-junit) flag to additionally store test results to XML format. To use this feature, you will need CMake >=3.21. The XML file is stored relative to the build directory.

A working `.circleci/config.yml` section for testing might look like the following example:

```yml
    steps:
      - checkout
      - run: mkdir build
      - run: cmake -S . -B build
      - run: ctest --test-dir build --output-junit out.xml
      - store_test_results:
          path: build/out.xml
```

### Bats for Bash

[Bats](https://bats-core.readthedocs.io/) provides a `--report-formatter junit` option to create a JUnit-format report in a location specified by the `--output` option. A subsequent `store_test_results` step can be passed to that same location.

The [`circleci/bats`](https://circleci.com/developer/orbs/orb/circleci/bats) orb’s [run job](https://circleci.com/developer/orbs/orb/circleci/bats?version=1.1.0#jobs-run) takes care of this functionality for you.

For example, a `.circleci/config.yml` section for running all `*.bats` tests within the `src/tests` folder might look like the following:

```yml
version: 2.1

orbs:
  bats: circleci/bats@1.1.0

workflows:
  test-my-app:
    jobs:
      - bats/run:
          formatter: junit
          path: ./src/tests
          timing: true
```

### GoogleTest

[GoogleTest](https://github.com/google/googletest) provides an XML option for its output flag, which will generate a JUnit XML report that can be used to upload and view test results on CircleCI.

```yml
    steps:
      - attach_workspace:
          at: .
      - run:
          name: Execute Tests
          command: |
            cd build
            ./tests --gtest_output=XML
      - store_test_results:
          path: build
```

You can see a full example on this [third party resource](https://levelup.gitconnected.com/how-to-combine-c-cmake-googletest-circleci-docker-and-why-e02d76c060a3).

### Go

To output JUnit XML for Go tests, use [gotestsum](https://github.com/gotestyourself/gotestsum). Add it to your module as a tool dependency with `go get -tool gotest.tools/gotestsum`, then run your tests through it:

```yml
    steps:
      - run:
          name: Run tests with JUnit output
          command: |
            mkdir -p test-results
            go tool gotestsum --junitfile=test-results/junit.xml -- ./...
      - store_test_results:
          path: test-results
```

### Playwright

```yml
    steps:
      - run:
          command: |
            mkdir test-results #can also be switched out for passing PLAYWRIGHT_JUNIT_OUTPUT_NAME directly to Playwright
            pnpm run serve &
            pnpm playwright test --config=playwright.config.ci.ts --reporter=junit

      - store_test_results:
          path: results.xml
```

Ensure that you are using version 1.34.2 or later of Playwright. Earlier versions of Playwright may not output JUnit XML in a format that is compatible with some of CircleCI’s testing features.

### Cypress

```yml
    steps:
      - run:
          command: |
            npm install --save-dev cypress-multi-reporters mocha-junit-reporter
            cypress run --reporter cypress-multi-reporters --reporter-options configFile=reporter-config.json ...<other options>

      - store_test_results:
          path: results.xml
```

### WebdriverIO

Update your wdio.conf.js to use the [JUnit reporter](https://webdriver.io/docs/junit-reporter/#configuration):

// wdio.conf.js
module.exports = {
    // ...
    reporters: \[
        'dot',
        \['junit', {
            outputDir: './test-results',
            outputFileFormat: function(options) { // optional
                return \`results-${options.cid}.${options.capabilities}.xml\`
            }
        }\]
    \],
    // ...
};

Update your `.circleci/config.yml` to upload the test results to CircleCI.

```yml
    steps:
      - run:
          command: |
            npm install @wdio/junit-reporter --save-dev
            wdio wdio.test.conf.js

      - store_test_results:
          path: ./test-results
```

## API

To access test metadata for a job from the API, refer to the [test-metadata API documentation](https://circleci.com/docs/api/v2/#operation/getTests).

## See also

*   [Using Insights](https://circleci.com/docs/guides/server/insights/insights/)