Skip to main content
SeleniumDecoded

Take a Screenshot on Test Failure Automatically

A framework hook for JUnit 5, pytest, Mocha and NUnit that saves a screenshot and page source only when a test fails.

Stable Verified against Selenium 4.48.0

Problem

Failure screenshots are the single most useful debugging artifact, and the most commonly forgotten. Adding them to every test by hand does not scale.

Solution

Hook the test framework’s after-test lifecycle, check the outcome, and save evidence named by the test.

Failure hook per framework
Selenium 4 Stable
// JUnit 5: a TestWatcher extension, registered once
public class ScreenshotOnFailure implements TestWatcher {
@Override
public void testFailed(ExtensionContext ctx, Throwable cause) {
WebDriver driver = DriverManager.get(); // your ThreadLocal accessor
String name = ctx.getRequiredTestClass().getSimpleName() + "." + ctx.getRequiredTestMethod().getName();
try {
Path dir = Files.createDirectories(Path.of("target", "failures"));
Files.write(dir.resolve(name + ".png"), ((TakesScreenshot) driver).getScreenshotAs(OutputType.BYTES));
Files.writeString(dir.resolve(name + ".html"), driver.getPageSource());
Files.writeString(dir.resolve(name + ".txt"), driver.getCurrentUrl() + System.lineSeparator() + cause);
} catch (IOException e) {
System.err.println("Could not save failure evidence: " + e.getMessage());
}
}
}
@ExtendWith(ScreenshotOnFailure.class)
class CheckoutTest { /* ... */ }
// TestNG equivalent: ITestListener.onTestFailure(ITestResult result)
conftest.py
import pytest
from pathlib import Path
@pytest.hookimpl(hookwrapper=True, tryfirst=True)
def pytest_runtest_makereport(item, call):
outcome = yield
report = outcome.get_result()
if report.when == "call" and report.failed:
driver = item.funcargs.get("driver")
if driver is None:
return
out = Path("failures"); out.mkdir(exist_ok=True)
name = item.nodeid.replace("::", ".").replace("/", "_")
driver.save_screenshot(out / f"{name}.png")
(out / f"{name}.html").write_text(driver.page_source, encoding="utf-8")
(out / f"{name}.txt").write_text(f"{driver.current_url}\n{call.excinfo.value}")
// Mocha root hook plugin: mocha --require ./hooks/screenshot-on-failure.js
const fs = require('fs');
const path = require('path');
exports.mochaHooks = {
async afterEach() {
if (this.currentTest.state !== 'failed') return;
const driver = this.currentTest.ctx.driver; // set by your beforeEach
if (!driver) return;
fs.mkdirSync('failures', { recursive: true });
const name = this.currentTest.fullTitle().replace(/[^a-z0-9]+/gi, '_');
fs.writeFileSync(path.join('failures', `${name}.png`), await driver.takeScreenshot(), 'base64');
fs.writeFileSync(path.join('failures', `${name}.html`), await driver.getPageSource());
fs.writeFileSync(path.join('failures', `${name}.txt`), `${await driver.getCurrentUrl()}\n${this.currentTest.err}`);
},
};
// NUnit: base class TearDown runs after every test
public abstract class WebTestBase
{
protected IWebDriver Driver = null!;
[SetUp] public void StartDriver() => Driver = DriverFactory.Create();
[TearDown]
public void CaptureAndQuit()
{
var ctx = TestContext.CurrentContext;
if (ctx.Result.Outcome.Status == TestStatus.Failed)
{
Directory.CreateDirectory("failures");
var name = ctx.Test.FullName.Replace('(', '_').Replace(')', '_');
((ITakesScreenshot)Driver).GetScreenshot().SaveAsFile(Path.Combine("failures", name + ".png"));
File.WriteAllText(Path.Combine("failures", name + ".html"), Driver.PageSource);
File.WriteAllText(Path.Combine("failures", name + ".txt"), Driver.Url + Environment.NewLine + ctx.Result.Message);
TestContext.AddTestAttachment(Path.Combine("failures", name + ".png"));
}
Driver.Quit();
}
}

Why It Works

Each framework exposes the test outcome to an after-hook before the driver is quit. Capturing there means the browser still shows the failing state. Naming files by test id makes them easy to find in CI artifacts.

Gotchas

  • Capture before quit(). If the driver fixture tears down first (pytest fixture order, JUnit extension order), move the capture into the fixture’s teardown.
  • Wrap the capture in try/catch; a crashed browser must not turn one failure into two.
  • Page source can contain secrets; skip it on auth pages or scrub tokens.
  • Upload the failures/ folder as a CI artifact; see CI/CD Pipelines.

Learn the theory