RSpec test reporting
RSpec gives you green dots, a failure list, and a seed number you will need later and did not write down. Qualflare turns those runs into hosted, historical reporting using RSpec’s own JSON formatter — no extra gem: every example as its own case, AI clustering of failures by root cause, and flaky scores that make order-dependent failures visible instead of mysterious.
RSpec’s formatters, and which one to keep
RSpec ships several formatters and — unusually — lets you run more than one at a time, each with its own destination. That is the whole integration: keep the console output you already read, and add a machine-readable file beside it.
progress. The default green dots. Compact, fast to scan, produces no artifact.documentation. Nested example names as prose — the output that makes people like RSpec. Still just console text.json. The complete run as structured data: every example with its ID, full description, status, run time and exception. This is what to upload, and it ships withrspec-core— no gem to add.rspec_junit_formatter(third-party gem). JUnit XML for CI plugins that only speak that format. Strictly lossier than the JSON: the schema has nowhere to put RSpec’s pending-versus-skipped distinction.
The JSON the formatter writes looks like this:
{
"examples": [
{
"id": "./spec/example_spec.rb[1:1]",
"description": "redirects on success",
"full_description": "Auth login redirects on success",
"status": "failed",
"file_path": "./spec/example_spec.rb",
"line_number": 12,
"run_time": 0.0412,
"exception": { "class": "RSpec::Expectations::ExpectationNotMetError", ... }
}
],
"summary_line": "3 examples, 1 failure, 1 pending"
} Why one JSON file isn’t reporting
Because the interesting questions in a Ruby suite are all historical ones. RSpec runs examples in random
order by default, which is a genuinely good decision — it surfaces leaked state that a fixed order would
hide — but it also means a failure you saw this morning may not reproduce this afternoon. One JSON file
cannot tell you whether "rejects a bad password" fails
one run in twenty or has been failing steadily since Tuesday, and that difference is the entire triage
decision.
For history you need something that stores results over time and analyzes them.
Send RSpec results to Qualflare
Add the JSON formatter to the command you already run. Nothing in
spec_helper.rb or your specs changes:
# RSpec's built-in JSON formatter, written to a file
rspec --format json --out rspec-results.json
# Keep readable console output at the same time
rspec --format progress --format json --out rspec-results.json Then upload it:
qf my-project collect rspec-results.json --format rspec In CI that’s one changed line and one added step. Authenticate the CLI once with your Qualflare access token, stored as a CI secret — see the CLI docs.
# .github/workflows/tests.yml
- name: Run specs
run: bundle exec rspec --format progress --format json --out rspec-results.json
- name: Upload results to Qualflare
if: always() # upload even when specs fail — that's the point
run: qf my-project collect rspec-results.json --format rspec
The if: always() line is the one people leave out.
rspec exits non-zero on failures, so without it the upload
step never runs on exactly the builds worth recording, and your failure history stays empty.
What you get on top of the dots
- Order-dependent flakiness made visible. Random ordering turns leaked state into intermittent failures; history turns intermittent failures into a score you can act on.
- Renames keep their history. RSpec keys an example on position, not description, so rewording an
itblock does not orphan its past. - AI failure clustering. When one broken factory or a missing fixture takes out 40 examples, they group into one root cause instead of 40 stack traces.
- Pending vs skipped kept apart. An example that was expected to fail and a spec someone excluded months ago are different problems, and stay different in the report.
- Per-example timing trends.
run_timecomes through on every case, so a slow creep is visible before it becomes a CI budget problem. - History, trends & defects. Pass rate, slowest examples, and flakiness over time across branches.
RSpec output vs Qualflare
| rspec --format json | Qualflare | |
|---|---|---|
| History across CI runs | — | Yes |
| Flaky scoring across random seeds | — | Yes |
| AI failure clustering (root cause) | — | Yes |
| Merges parallel_tests / sharded jobs | — | Yes |
| Full backtrace for one run | Yes | Yes |
| Local, zero-setup, offline | Yes | — |
Complementary: keep --format documentation for local runs,
add Qualflare for hosted, historical CI observability.
Get AI analysis on your RSpec runs
Start free — add --format json, run qf collect, and get your first AI analysis in minutes.
Qualflare works the same with pytest, Jest, Go, TestNG, Playwright and 20+ more frameworks. Chasing a specific flaky spec? See RSpec flaky tests: --bisect, --seed, and let vs let!. Weighing tools? See how it compares to other test management platforms, or browse all framework reporting guides. Every reporter is open source.
Example IDs, seeds, and the flakiness RSpec goes looking for
RSpec identifies an example by position, not name. An ID like
./spec/auth_spec.rb[1:1:2] means the second example inside
the first nested block inside the first top-level group. That has a pleasant consequence and an awkward one:
# spec/auth_spec.rb
describe "Auth" do
describe "login" do
it "redirects on success" do ... end # [1:1:1]
it "rejects a bad password" do ... end # [1:1:2] <- insert above this
end # and it becomes [1:1:3]
end The pleasant one: rewording an example keeps its history. In most frameworks the test name is the identity, so improving a description silently retires the old test and starts a new one with no past. RSpec is immune to that. The awkward one: inserting an example shifts every ID below it, so those examples look brand new and their history restarts. It is not data loss — the old records remain — but the continuity breaks. Where it is natural to do so, add new examples at the end of a block.
Then there is ordering. RSpec runs in random order by default and prints the seed every time:
$ rspec
Randomized with seed 41234 # different every run, by design
$ rspec --seed 41234 # reproduce that exact order
$ rspec --bisect --seed 41234 # find the minimal spec set that reproduces it This is RSpec deliberately hunting for a bug class most suites never look for. If example A leaves a record in the database, a stubbed constant, or a memoised singleton behind, example B may pass only when it runs after A — a suite in fixed order stays green and breaks the day someone reorders a file. Random ordering converts that latent fault into an intermittent failure, which is uncomfortable and correct.
It is also why RSpec suites benefit disproportionately from history. A single random-order failure is nearly
useless on its own — you cannot tell an order dependency from a genuine regression from noise. Across fifty
runs the shape is obvious: an order-dependent example fails at a rate set by how often the ordering puts its
partner first, while a real regression fails every time from a particular commit onward. Record the seed
from a failing run, feed it to --bisect, and RSpec will
narrow it to the minimal set of specs that reproduce it.
Two smaller notes. Pending and skipped are not the same thing — a
skipped example never ran, a pending one ran and was expected to fail, and a pending example that
unexpectedly passes is RSpec telling you the fix has landed. And
--only-failures needs
configuration: it depends on
example_status_persistence_file_path, because RSpec has to
remember the previous outcome somewhere. If that file is not persisted between CI jobs, the flag silently
has nothing to work from.
Frequently asked questions
How do I send RSpec results to Qualflare?
Use RSpec’s built-in JSON formatter — rspec --format json --out rspec-results.json — then upload with qf my-project collect rspec-results.json --format rspec. You can pass --format twice to keep readable console output alongside the file. No gem, no spec_helper change, and no rewriting of your specs is required.
Do I need the rspec_junit_formatter gem?
No. RSpec’s own JSON formatter ships with rspec-core and carries more than JUnit XML can express — per-example run_time, the exception class and message, and RSpec’s distinction between pending and skipped. If you already produce JUnit XML for a CI plugin you can upload that instead with --format junit, but you lose that detail, so prefer the native JSON.
Does reordering my spec file reset a test’s history?
It can, and this is RSpec’s one real quirk. An example ID like ./spec/auth_spec.rb[1:1:2] is positional — the numbers are the example’s index within its nested blocks, not its name. Renaming an example keeps its ID, which is the opposite of most frameworks and genuinely useful. But inserting a new example above an existing one shifts every ID below it, so those examples look new and their history starts over. Insert at the end of a block when you can.
How does random ordering affect flaky detection?
It helps, and it is the reason RSpec suites surface order-dependent flakiness that other suites hide. Running with --order random means leaked state between examples shows up as an intermittent failure instead of a permanently green suite that would break the moment anything moved. Record the seed — RSpec prints it on every run — because it is what makes the failure reproducible, and it is what you pass to --bisect to find the minimal pair of specs involved.
Does Qualflare distinguish pending from skipped examples?
Yes, because RSpec does and the JSON formatter carries it. A skipped example was never run; a pending one was run and was expected to fail. The distinction matters in a report — a pending example that unexpectedly passes is RSpec telling you the fix landed and the marker should come off, which is a different signal from a spec someone excluded months ago and forgot.
Does RSpec retry failed examples?
Not out of the box — retries come from the separate rspec-retry gem, so by default there is no in-run attempt data to read and flakiness must come from history across runs, which is the more accurate method anyway. The related built-in is --only-failures, which re-runs just what failed last time; it requires example_status_persistence_file_path to be configured, since RSpec needs somewhere to remember the previous outcome.
Setup reflects the Qualflare CLI
(docs.qualflare.com)
as of September 2026. Formatter and ordering behaviour follows
RSpec's own documentation;
the JSON shape and the ./spec/file_spec.rb[1:1] example-id format are taken from the CLI's
RSpec parser and its fixtures. Written by İbrahim Süren, Qualflare.