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 oncepublic 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)import pytestfrom 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.jsconst 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 testpublic 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
- Screenshots and Videos Capture screenshots and record videos during test execution for debugging and reporting.
- Reporting: Allure, ExtentReports and JUnit XML Turn raw test output into reports people read: JUnit XML for CI, Allure for rich history and attachments, ExtentReports for single-file HTML, with screenshots, logs and steps attached automatically.