A long-lived project that still receives updates
RSpec results that your continuous integration service can read.
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
2026
 Dependencies

Development

>= 0
~> 1.8, >= 1.8.2

Runtime

>= 3.13, < 4
 Project Readme

RSpec JUnit Formatter

Build results Gem version

RSpec 3 results that your CI can read. Jenkins, Buildkite, CircleCI, Gitlab, and probably more, too.

Usage

Requires Ruby >= 3.3 and RSpec >= 3.13, < 4. For older versions, see legacy support.

Install the gem:

gem install rspec_junit_formatter

Use it:

rspec --format RspecJunitFormatter --out rspec.xml

You'll get an XML file rspec.xml with your results in it.

You can use it in combination with other formatters, too:

rspec --format progress --format RspecJunitFormatter --out rspec.xml

Using in your project with Bundler

Add it to your Gemfile if you're using Bundler. Put it in the same groups as rspec.

group :test do
  gem "rspec"
  gem "rspec_junit_formatter", require: false
end

Put the same arguments as the commands above in your .rspec:

--format RspecJunitFormatter
--out rspec.xml

Parallel tests

For use with parallel_tests, add $TEST_ENV_NUMBER in the output file option (in .rspec or .rspec_parallel) to avoid concurrent process write conflicts.

--format RspecJunitFormatter
--out tmp/rspec<%= ENV["TEST_ENV_NUMBER"] %>.xml

The formatter appends $TEST_ENV_NUMBER to the test suite name within the XML, too. An explicitly set value always wins, including the empty string that parallel_tests uses for its first worker: TEST_ENV_NUMBER="" keeps the name rspec.

When TEST_ENV_NUMBER is unset, the formatter uses the first non-empty value from this table, in order:

Variable Provider Native first index
CI_NODE_INDEX GitLab, or a user-supplied CI label 1 on GitLab
CIRCLE_NODE_INDEX CircleCI 0
SEMAPHORE_JOB_INDEX Semaphore 1
BUILDKITE_PARALLEL_JOB Buildkite 0
BITBUCKET_PARALLEL_STEP Bitbucket Pipelines 0

Values are labels, not test-sharding offsets: they are XML-escaped but otherwise unchanged. For example, GitLab's CI_NODE_INDEX=1 produces rspec1, while Buildkite's BUILDKITE_PARALLEL_JOB=0 produces rspec0. There is no index conversion based on GITLAB_CI, and a custom CI_NODE_INDEX takes precedence over the other CI variables. If all these values are empty or unset, the name stays rspec; no default 0 is appended.

The old SEMAPHORE_CURRENT_THREAD and SNAP_WORKER_INDEX variables are not detected: Semaphore Classic was retired in 2023 and Snap CI in 2017. Other runners can set TEST_ENV_NUMBER explicitly.

This changes only the XML suite name, not the output filename. Continue to choose distinct --out paths for processes sharing a filesystem. CI indexes are unique only within their parallel job group, not across steps, builds, or nested parallel_tests workers. To combine those reports, set an explicit label that includes each relevant dimension, and use it in the filename too. For example, inside each parallel_tests worker on Buildkite:

export TEST_ENV_NUMBER="-${BUILDKITE_STEP_KEY}-${BUILDKITE_PARALLEL_JOB}-${TEST_ENV_NUMBER}"
bundle exec rspec --format RspecJunitFormatter --out "tmp/rspec${TEST_ENV_NUMBER}.xml"

Use distinct Buildkite step keys for this example; add a build identifier too if you combine reports across builds.

Capturing output

If you like, you can capture the standard output and error streams of each test into the :stdout and :stderr example metadata which will be added to the junit report, e.g.:

# spec_helper.rb

RSpec.configure do |config|
  # register around filter that captures stdout and stderr
  config.around(:each) do |example|
    $stdout = StringIO.new
    $stderr = StringIO.new

    example.run

    example.metadata[:stdout] = $stdout.string
    example.metadata[:stderr] = $stderr.string

    $stdout = STDOUT
    $stderr = STDERR
  end
end

Note that this example captures all output from every example all the time, potentially interfering with local debugging. You might like to restrict this to only on CI, or by using rspec filters.

Caveats

  • XML can only represent a limited subset of characters which excludes null bytes and most control characters. This gem will use character entities where possible and fall back to replacing invalid characters with Ruby-like escape codes otherwise. For example, the null byte becomes \0.
  • When converting report text to UTF-8 for XML output, invalid byte sequences and characters with no UTF-8 conversion are replaced with � (U+FFFD REPLACEMENT CHARACTER). The formatter uses each string's declared encoding; it does not guess the encoding of binary data.

Supported versions and legacy upgrades

We support maintained Ruby versions, including security maintenance, and the current stable RSpec series. Minor releases may raise the minimum versions when upstream support ends.

For Ruby < 3.3 or RSpec < 3.13, use 0.6.2, the final legacy release:

gem "rspec_junit_formatter", "0.6.2", require: false

Run bundle update rspec_junit_formatter, or install without Bundler using gem install rspec_junit_formatter -v 0.6.2. This release permits Ruby >= 2.3 and RSpec >= 2, < 4, except 2.12.0; see its tested combinations.

Contributing

See CONTRIBUTING.md for development, testing and release instructions.

License

The MIT License, see LICENSE.

Thanks

Inspired by the work of Diego Souza on RSpec Formatters after frustration with CI Reporter.