Skip to content
advanced Phase 12 · CI/CD Quality Gates & Reporting

Test Reporting and Dashboards

Publish rich HTML reports, trend charts, and attached failure artifacts to stakeholders.

1h
0 problems
Topic Progress 0%

From JUnit XML to a Report Someone Actually Opens: Allure in the Pipeline

CI already produces results — JUnit XML, a wall of green and red — and almost nobody reads them, because a raw XML file is not an answer to the question a person has: what failed, why, and is it my change? An Allure report turns those results into a browsable HTML page with suites, histories, retries, attachments, and trends, and publishing it is a handful of commands.

npm i -D allure-js-commons allure-commandline allure-playwright
// playwright.config.ts — the reporter writes raw results, CI turns them into HTML
export default defineConfig({
  reporter: [
    ["list"],
    ["allure-playwright", { detail: true, outputFolder: "allure-results" }],
    ["junit", { outputFile: "results/junit.xml" }],   // keep the XML for machines
  ],
});
npx allure generate allure-results -o allure-report --clean
npx allure open allure-report            # local preview
npx allure serve allure-results          # one-shot: generate + open + expire

The distinction between the two outputs is worth internalizing: allure-results is the machine format (one JSON file per test plus attachments), and allure-report is the generated site. CI consumes the former to produce the latter, then publishes it. Two wiring details decide whether anyone ever sees it:

- name: Build and publish the report
  if: always()                    # reports matter most when the run failed
  run: npx allure generate allure-results -o allure-report --clean
- name: Deploy report
  if: always()
  uses: peaceiris/actions-gh-pages@v3
  with:
    github_token: ${{ secrets.GITHUB_TOKEN }}
    publish_dir: ./allure-report
- uses: actions/upload-artifact@v4
  with: { name: allure-results, path: allure-results/ }

Publish it at a stable URL (https://qa.example.com/allure/) served by CI, not as a downloadable ZIP — an artifact someone must fetch and unzip locally gets opened once. if: always() is the other non-negotiable: a report generated only on success documents the happy runs and omits exactly the failures people need. Keep junit.xml alongside for machines (gates, dashboards, analytics parse XML/JSON, not HTML), and cache or copy allure-results/history between runs — that folder is what buys you the trend charts in the next chapter. Playwright's own html reporter is a perfectly good lighter alternative for a single-project suite; Allure earns its keep with attachments, history, categories, and cross-framework aggregation. Stamp the run with metadata, or trends and drill-downs have nothing to join on. Allure reads two small files dropped into allure-results:

cat > allure-results/executor.properties <<EOF
name=github-actions
buildName=${GITHUB_SHA::8}
buildUrl=${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}
reportName=Shop API and E2E
EOF
echo "base_url=$BASE_URL" > allure-results/environment.properties
echo "browser=chromium"  >> allure-results/environment.properties

Those lines make a report clickable back to the exact CI run that produced it — one click from a failed assertion to the logs, the diff, and the commit — and they record which environment the results came from (base URL, browser, OS), so nobody relitigates whether a failure was on staging or on production. It is a handful of lines, and it is the highest-value part of the whole configuration.

Whatever the tool, the habit is the same — every run produces a report, the report survives the run, and its URL never changes. Teams that adopt this once stop debating whether a run was "clean": the answer is one link away, with the same address today as it had six months ago.

One Dataset, Two Audiences: Triage Evidence for Engineers, Status for Stakeholders

The same run has to serve two readers with nothing in common: an engineer asking which assertion, on which build, with what evidence? and a product lead asking can we ship, and what is the risk? Build both from one dataset, but do not show the same screen to both — an engineer given an executive summary and a stakeholder given a failure log each waste their time.

For triage, evidence must be attached to the failed test, not stored three clicks away.

// playwright + allure: attach at the moment of failure
await test.info().attach("response.json", {
  body: JSON.stringify(await res.json(), null, 2),
  contentType: "application/json",
});
await test.info().attach("screenshot", { path: testInfo.outputPath("failure.png") });
await test.info().attach("console.log", { body: browserLogs.join("\n"), contentType: "text/plain" });

The default set for a failed test is screenshot, video, console/browser logs, the request and response pair, and the seed data or fixture ids — enough that triage starts in the report instead of in a local reproduction attempt. For device-farm runs, attach the vendor session URL rather than the video blob (the pattern from Device Farms in CI). Then add categories, which turn a wall of red into classified work:

[
  { "name": "Product defect", "matchedStatuses": ["failed"], "messageRegex": ".*expected.*" },
  { "name": "Test bug", "messageRegex": ".*TimeoutError.*" },
  { "name": "Infrastructure", "messageRegex": ".*device.*unavailable.*" },
  { "name": "Known issue", "flaky": true, "expires": "2026-12-01" }
]

For stakeholders, publish status, not logs. What belongs on the shared dashboard is: open/blocking failures against the release candidate, trend of pass rate and flaky rate, coverage or gate status, quarantine health, and the top five risks with an owner each — at a stable URL that is always current, which is the real answer to "communicate quality without meetings". The reporting principle that keeps it honest: never present a raw failure count without classification, because three failures that are all known environment issues should not read worse than one new defect, and a green suite hiding 40 quarantined tests should not read better than it is. Label the data by build and by environment, stamp "as of " on every view, and route questions about a number to the drill-down page rather than to a person.

Done well, this closes the loop the whole phase has been building: the pipeline gates the merge, the gate publishes numbers, the numbers carry evidence, and the dashboard shows the same numbers to everyone — one dataset, one story, no meeting required.

Wire the report into the flow rather than hoping it gets consulted: post the report URL with the classified summary as a PR comment when checks fail, put the failing test's deep link in the CI annotation, and include it in the nightly message. Every link must be deep — straight to that test's history page, not the report's front door — because the extra click is where triage goes to die. Then close the loop with a classification step: the engineer sees the evidence, labels it defect / test bug / infrastructure, and the label feeds the categories in the next run — so the "known issues" bucket is maintained by whoever triaged, instead of by a separate reporting chore nobody schedules.

The next topic picks up the first number most teams measure after functional correctness: Performance Concepts and Baselines — how fast the system is.

Quiz

1. Why must the report-generation step in CI use `if: always()` (or run on failure too)?

Question 1 options

2. A team renames tests from `test17` to descriptive titles and the trend chart resets. What is the root cause and the fix?

Question 2 options

3. The dashboard shows pass rate rising to 99% while test count dropped by 120 this sprint. What should you conclude?

Question 3 options

4. What should a stakeholder-facing quality dashboard show, given the same underlying results?

Question 4 options

Flashcards

Question

allure-results vs allure-report — what each is and who consumes it.

Answer

allure-results is the raw machine format (one JSON per test + attachments) produced by the reporter in CI; allure-report is the generated static HTML site produced by `allure generate allure-results -o allure-report --clean`. CI consumes results, publishes the site at a stable URL, and keeps junit.xml for gates and other tooling.

Question

What is historyId, and what habit protects trend charts?

Answer

The key Allure derives from a test's name and parameters to link runs across builds. Renaming resets the series (and parameterized runs fragment if values vary). Habit: behavior-based names, rare deliberate renames, explicit stable ids where supported — plus consistent worker/shard counts so duration trends stay comparable.

Question

The four trend metrics worth charting, and what each signals.

Answer

Pass rate (dip = regression; sawtooth = flaky/unstable env) · duration p50/p95 (creep = suite growth or slow environment) · flaky count/rate (spike after parallelization = shared state; gradual rise = debt) · retries consumed (rising = tests degrading before failing outright). Read them with test count and quarantine health so no ratio can be gamed.

Question

Default attachments for a failed test, and the device-farm variation.

Answer

Screenshot, video, console/browser logs, request+response pair, and seed/fixture ids — attached at failure so triage starts inside the report. On a device farm, attach the vendor session URL instead of the video blob (storage/bandwidth are metered), plus device log and Appium log links.

Question

What belongs on a stakeholder quality dashboard — and what must never be shown raw?

Answer

Classified blocking vs known-issue failures for the release candidate, pass/flaky trends, gate status, quarantine health, and top risks each with an owner — at a stable always-current URL stamped with build/commit. Never a raw failure count (known issues look catastrophic) or a green number hiding quarantined tests (overstates confidence).

Revision Notes

Key Takeaways

  • 1. Every run generates a report with `if: always()`, published at a URL that never changes — an artifact nobody unzips is not reporting
  • 2. allure-results is for machines, allure-report is for humans; keep junit.xml too, because gates parse XML rather than HTML
  • 3. Trends depend on stable test identity: renaming tests silently resets the history everyone uses for decisions
  • 4. Attach evidence at the moment of failure — screenshot, logs, request/response — so triage starts in the report
  • 5. One dataset, two audiences: classified status with owners for stakeholders, full drill-down for engineers; never a raw count either way

Interview Tips

  • • "How do you report test results?" — reporter writes results, CI generates HTML with if: always(), publishes at a stable URL, keeps XML for gates; mention history preservation for trends
  • • "How do you show quality to non-engineers?" — classified view (blocking vs known), trends, gate status, top risks with owners, always-current URL; argue against raw failure counts
  • • "Why did our flaky trend reset?" — historyId derives from name + parameters, so renames or varying parameters fragment the series; fix is stable ids and naming discipline
  • • "Attach what on failure?" — screenshot, video, console, request/response, seed data; on device farms link the vendor session instead of copying video blobs

Cheat Sheet

Test Reporting and Dashboards

Setup:

reporter: [["list"], ["allure-playwright", { detail: true, outputFolder: "allure-results" }],
           ["junit", { outputFile: "results/junit.xml" }]]
npx allure generate allure-results -o allure-report --clean   # site
npx allure serve allure-results                                # generate + open + expire

CI wiring: generate with if: always() → publish at a stable URL (gh-pages / internal host) → upload allure-results as an artifact → preserve allure-results/history between runs for trends.

Trends: historyId comes from name + parameters → keep behavior-based names, avoid renames, stable ids, stable parameter values. Chart pass rate · duration p50/p95 · flaky rate · retries consumed; annotate worker/hardware changes; always show test count + quarantine health next to pass rate.

Attachments (on failure): screenshot · video · console logs · request/response JSON · seed ids. Device farm → attach vendor session URL, not the video file.

Categories JSON classifies failures: Product defect · Test bug · Infrastructure · Known issue (with expiry) — turns a wall of red into triage buckets.

Two views, one dataset: engineers get drill-down evidence in the report; stakeholders get classified status (blocking vs known), trends, gate results, quarantine health, top risks with owners) at an always-current URL stamped with build and date.

Continue Learning