RSpec JUnit Formatter
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_formatterUse it:
rspec --format RspecJunitFormatter --out rspec.xmlYou'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.xmlUsing 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
endPut the same arguments as the commands above in your .rspec:
--format RspecJunitFormatter
--out rspec.xmlParallel 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"] %>.xmlThe 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
endNote 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: falseRun 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.