PRACTICAL GUIDE / FluentWait custom Clock Sleeper testing
Test FluentWait polling without waiting on the wall clock
Inject a controlled Clock and Sleeper into Selenium FluentWait, verify polling and timeout paths quickly, and keep real-browser timing tests honest.
In this guide8 sections
- Know which part of FluentWait you are testing
- Build a clock and sleeper that move together
- Prove success and timeout as different schedules
- Model condition cost and interruption deliberately
- Prove the wait used the clock you supplied
- Diagnose controlled-time tests that give misleading results
- Introduce controlled time without weakening browser coverage
- What controlled time costs, and when not to use it
What you will learn
- Know which part of FluentWait you are testing
- Build a clock and sleeper that move together
- Prove success and timeout as different schedules
- Model condition cost and interruption deliberately
A wait-helper test spends its entire configured timeout proving that a timeout works. Multiply that by success, failure, and interruption cases, and a tiny utility suite becomes slow and inconsistent. The browser is not under test in those cases; the polling policy is.
Selenium's Java FluentWait constructor accepts a Clock and a Sleeper. A controlled implementation can advance logical time instantly, record every sleep request, and make boundary behavior repeatable. It cannot tell you how fast a real page, thread scheduler, or Grid behaves.
Know which part of FluentWait you are testing
FluentWait<T> holds an input value, timeout, polling interval, ignored-exception list, message supplier, clock, and sleeper. Its until() loop evaluates a function with the input. A non-null, non-false result succeeds. An unignored exception escapes. Otherwise the loop checks the deadline, asks the sleeper to pause for the configured interval, and tries again.
The public constructor FluentWait(T input, Clock clock, Sleeper sleeper) makes the time boundary replaceable. The Clock answers instant() when the wait establishes and checks its deadline. The Sleeper receives a Duration between evaluations.
That design supports a narrow unit test. You can prove that the first condition evaluation happens before a sleep, that false results lead to the configured sleep request, that success stops polling, that the message contains configured timeout information, and that interruption is propagated according to Selenium's behavior.
It does not make the condition itself pure or fast. A condition can still issue WebDriver commands, wait inside application code, block on network activity, or mutate state. Replacing wall time while those operations remain real creates a hybrid test that is difficult to interpret.
Separate three layers:
- Wait-loop policy: clock reads, requested sleeps, success values, ignored exceptions, timeout messages, and interruption.
- Condition logic: how a domain state is read and which value means success.
- Browser integration: actual DOM updates, remote command latency, rendering, and infrastructure.
Controlled time is strongest at the first layer. A local static page can exercise the second. A real browser or Grid run is needed for the third.
The distinction prevents fabricated performance claims. If a fake sleeper advances 250 milliseconds instantly, the test proves that FluentWait requested that duration. It did not observe a 250 millisecond pause, and it did not show that polls in CI start 250 milliseconds apart.
Build a clock and sleeper that move together
The controlled clock must be mutable for the test, but Selenium only receives it through the standard java.time.Clock interface. The sleeper records each duration and advances that same clock instead of blocking the thread.
import java.time.Clock;
import java.time.Duration;
import java.time.Instant;
import java.time.ZoneId;
import java.util.ArrayList;
import java.util.List;
import org.openqa.selenium.support.ui.Sleeper;
final class MutableClock extends Clock {
private Instant current;
private final ZoneId zone;
MutableClock(Instant start, ZoneId zone) {
this.current = start;
this.zone = zone;
}
@Override
public ZoneId getZone() {
return zone;
}
@Override
public Clock withZone(ZoneId newZone) {
return new MutableClock(current, newZone);
}
@Override
public Instant instant() {
return current;
}
void advance(Duration duration) {
if (duration.isNegative()) {
throw new IllegalArgumentException("Clock cannot move backwards");
}
current = current.plus(duration);
}
}
final class RecordingSleeper implements Sleeper {
private final MutableClock clock;
private final List<Duration> requests = new ArrayList<>();
RecordingSleeper(MutableClock clock) {
this.clock = clock;
}
@Override
public void sleep(Duration duration) {
requests.add(duration);
clock.advance(duration);
}
List<Duration> requests() {
return List.copyOf(requests);
}
}Both classes should be test fixtures, not production infrastructure. MutableClock rejects negative advances because timeout tests need a forward-moving timeline. Its withZone() method returns another clock at the same instant; these tests use UTC and do not share mutable state across zones.
The sleeper records requested values before advancing time. If an assertion fails, the list shows whether the wait asked for the expected interval and how many sleeps occurred. It does not claim the operating system slept for that duration.
Do not create a sleeper that records but leaves the clock frozen. An always-false condition will keep seeing the same instant and may loop indefinitely. Likewise, do not advance one Clock while passing a different instance to FluentWait.
Use a fresh clock and sleeper per test. Sharing them makes one test's logical time become another test's starting point, and parallel execution will interleave requests.
Prove success and timeout as different schedules
A success-path test should establish three facts: the condition is evaluated immediately, sleeps occur only between failed evaluations, and the returned value comes from the successful evaluation.
The durations below are explicit test inputs. They illustrate a schedule; they are not measurements from Selenium in production.
import static org.junit.jupiter.api.Assertions.assertEquals;
import java.time.Duration;
import java.time.Instant;
import java.time.ZoneOffset;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
import org.junit.jupiter.api.Test;
import org.openqa.selenium.support.ui.FluentWait;
class ControlledFluentWaitTest {
@Test
void returnsOnTheThirdEvaluationAndSleepsTwice() {
MutableClock clock = new MutableClock(
Instant.parse("2026-08-04T00:00:00Z"),
ZoneOffset.UTC);
RecordingSleeper sleeper = new RecordingSleeper(clock);
AtomicInteger evaluations = new AtomicInteger();
FluentWait<String> wait = new FluentWait<>("ORD-1048", clock, sleeper)
.withTimeout(Duration.ofSeconds(2))
.pollingEvery(Duration.ofMillis(250));
String result = wait.until(orderId ->
evaluations.incrementAndGet() == 3 ? orderId : null);
assertEquals("ORD-1048", result);
assertEquals(3, evaluations.get());
assertEquals(
List.of(Duration.ofMillis(250), Duration.ofMillis(250)),
sleeper.requests());
assertEquals(
Instant.parse("2026-08-04T00:00:00.500Z"),
clock.instant());
}
}The first evaluation happens at the starting instant. Two null results lead to two 250 millisecond sleep requests. The third returns the input and no further sleep occurs. The final instant is derived from the controlled advances, not from wall-clock elapsed time.
A timeout test asks different questions. It should prove that an always-false condition produces TimeoutException, every recorded sleep request uses the configured interval, and the diagnostic names the policy. Avoid asserting the full exception string because Selenium may add or format driver information differently across releases.
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.time.Duration;
import java.time.Instant;
import java.time.ZoneOffset;
import org.junit.jupiter.api.Test;
import org.openqa.selenium.TimeoutException;
import org.openqa.selenium.support.ui.FluentWait;
class ControlledTimeoutTest {
@Test
void timesOutWithoutSleepingTheTestThread() {
MutableClock clock = new MutableClock(
Instant.parse("2026-08-04T00:00:00Z"),
ZoneOffset.UTC);
RecordingSleeper sleeper = new RecordingSleeper(clock);
FluentWait<String> wait = new FluentWait<>("report-77", clock, sleeper)
.withTimeout(Duration.ofSeconds(1))
.pollingEvery(Duration.ofMillis(200))
.withMessage("report-77 to become downloadable");
TimeoutException failure = assertThrows(
TimeoutException.class,
() -> wait.until(ignored -> false));
assertFalse(sleeper.requests().isEmpty());
assertTrue(sleeper.requests().stream()
.allMatch(Duration.ofMillis(200)::equals));
assertTrue(failure.getMessage()
.contains("report-77 to become downloadable"));
assertTrue(failure.getMessage().contains("1 second"));
assertTrue(failure.getMessage().contains("200 milliseconds interval"));
}
}The test intentionally avoids a hard-coded number of evaluations at the deadline. Boundary details belong to the pinned Selenium version and may be affected by how deadline comparison is implemented. If a framework relies on an exact attempt count, write that expectation explicitly, pin the dependency, and review it on upgrade.
Most application tests should not rely on exact count. They care that the state becomes true within a product timeout. Exact polling tests are for the helper or library that promises a policy.
Model condition cost and interruption deliberately
Selenium documents that the polling interval can be exceeded because time spent evaluating the condition is not included in the sleep interval. A controlled clock can demonstrate the shape without pretending to benchmark it.
In the next test, each condition evaluation advances logical time by 300 milliseconds, and the sleeper adds the requested 200 milliseconds. The distance between the first two evaluation starts is therefore 500 milliseconds in this model.
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import java.time.Duration;
import java.time.Instant;
import java.time.ZoneOffset;
import java.util.ArrayList;
import java.util.List;
import org.junit.jupiter.api.Test;
import org.openqa.selenium.TimeoutException;
import org.openqa.selenium.support.ui.FluentWait;
class ConditionCostTest {
@Test
void conditionCostIsAddedToTheRequestedSleep() {
MutableClock clock = new MutableClock(
Instant.parse("2026-08-04T00:00:00Z"),
ZoneOffset.UTC);
RecordingSleeper sleeper = new RecordingSleeper(clock);
List<Instant> evaluationStarts = new ArrayList<>();
FluentWait<String> wait = new FluentWait<>("job-19", clock, sleeper)
.withTimeout(Duration.ofSeconds(1))
.pollingEvery(Duration.ofMillis(200));
assertThrows(TimeoutException.class, () -> wait.until(ignored -> {
evaluationStarts.add(clock.instant());
clock.advance(Duration.ofMillis(300));
return null;
}));
assertEquals(
Duration.ofMillis(500),
Duration.between(evaluationStarts.get(0), evaluationStarts.get(1)));
}
}The 300 millisecond advance is a simulation input. It demonstrates addition of evaluation cost and sleep. It says nothing about how long a real findElement() or remote Grid command takes.
Interruption needs a separate test. Current FluentWait catches InterruptedException from the Sleeper, restores the thread's interrupted flag, and throws WebDriverException. A test must clear the flag in finally so it does not contaminate later JUnit work on the same thread.
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.time.Duration;
import java.time.Instant;
import java.time.ZoneOffset;
import org.junit.jupiter.api.Test;
import org.openqa.selenium.WebDriverException;
import org.openqa.selenium.support.ui.FluentWait;
import org.openqa.selenium.support.ui.Sleeper;
class InterruptedSleeperTest {
@Test
void restoresTheInterruptedFlagAndWrapsTheCause() {
MutableClock clock = new MutableClock(
Instant.parse("2026-08-04T00:00:00Z"),
ZoneOffset.UTC);
Sleeper interrupted = duration -> {
throw new InterruptedException("simulated interruption");
};
try {
FluentWait<String> wait = new FluentWait<>("input", clock, interrupted)
.withTimeout(Duration.ofSeconds(1))
.pollingEvery(Duration.ofMillis(100));
WebDriverException failure = assertThrows(
WebDriverException.class,
() -> wait.until(ignored -> false));
assertTrue(Thread.currentThread().isInterrupted());
assertTrue(failure.getCause() instanceof InterruptedException);
} finally {
Thread.interrupted();
}
}
}This code does not call Thread.currentThread().interrupt() before the wait. It tests the Sleeper contract directly. A separate integration test is needed if the framework cancels workers through executors, futures, or build-tool timeouts.
Exception-ignore policy also deserves focused coverage. Configure one known transient exception, prove the condition is retried, and prove a different exception escapes immediately. Do not add RuntimeException or WebDriverException to a broad ignore list merely to make a timeout test pass.
Plain FluentWait begins with no ignored exceptions. WebDriverWait adds NotFoundException to its policy because element lookup commonly fails while a page is still changing. A framework that tests FluentWait with a string input must configure the transient exception it expects; otherwise the first throw correctly ends the wait.
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import java.time.Duration;
import java.time.Instant;
import java.time.ZoneOffset;
import java.util.concurrent.atomic.AtomicInteger;
import org.junit.jupiter.api.Test;
import org.openqa.selenium.NoSuchElementException;
import org.openqa.selenium.support.ui.FluentWait;
class IgnoredExceptionPolicyTest {
@Test
void retriesOnlyTheWhitelistedException() {
MutableClock clock = new MutableClock(
Instant.parse("2026-08-04T00:00:00Z"),
ZoneOffset.UTC);
RecordingSleeper sleeper = new RecordingSleeper(clock);
AtomicInteger calls = new AtomicInteger();
FluentWait<String> wait = new FluentWait<>("invoice-31", clock, sleeper)
.withTimeout(Duration.ofSeconds(1))
.pollingEvery(Duration.ofMillis(100))
.ignoring(NoSuchElementException.class);
String result = wait.until(input -> {
int call = calls.incrementAndGet();
if (call == 1) {
throw new NoSuchElementException("status not rendered");
}
return call == 3 ? input : null;
});
assertEquals("invoice-31", result);
assertEquals(3, calls.get());
FluentWait<String> strictWait = new FluentWait<>(
"invoice-32",
new MutableClock(Instant.EPOCH, ZoneOffset.UTC),
duration -> {});
assertThrows(
IllegalStateException.class,
() -> strictWait.until(input -> {
throw new IllegalStateException("invoice was rejected");
}));
}
}The second sleeper does nothing, but the unignored exception occurs on the first evaluation, before a sleep or timeout check can be relevant. That makes the test safe from a frozen-clock loop. If the condition returned false instead, this fixture would be invalid because logical time would never advance.
Test the ignored-exception timeout separately from success. Current FluentWait can attach the most recent ignored exception as the timeout cause, but a later null or false result clears that cause. A helper that needs a complete transition history should record a bounded domain trace in the condition. Changing the clock cannot recover information the condition discarded.
When the production abstraction is WebDriverWait, add one narrow construction test using its five-argument constructor: driver, timeout, sleep duration, Clock, and Sleeper. That verifies the wait factory passes policy through correctly. Keep the WebDriver mock or controlled driver focused on construction; use a browser fixture to prove actual ExpectedCondition behavior. A deep mock of every WebDriver command usually tests the mock more than the condition.
Prove the wait used the clock you supplied
A controlled-time timeout and a wall-clock timeout can print the same configured duration and polling interval. This happens when a wait factory accepts a test clock and sleeper but constructs a different wait internally, or when one call path falls back to the ordinary constructor. The test still fails with TimeoutException, so its assertion passes after spending real time. The defect is fixture wiring, not FluentWait's timeout policy.
The timeout message cannot separate the cases because it reports configuration, not which Clock and Sleeper served the loop. Record the controlled starting instant, controlled ending instant, evaluation starts, requested sleep durations, exception type and cause, and wall elapsed time under a label that says it is wall time. In a healthy controlled timeout, the sleeper list is nonempty, every ordinary request matches the configured interval, and the controlled time advance equals the recorded sleeps plus any condition cost the fixture deliberately modeled. Wall time should be treated only as a broad guard against accidental blocking.
Broken wiring has a different signature. The injected clock ends where it started and the injected sleeper records no requests, while the test consumes wall time before producing the configured timeout text. A misleading case also has no sleep requests: an unignored exception can escape on the first evaluation before the sleeper is needed. Read the evaluation count and exception cause before calling that a wiring failure. Immediate success is another healthy no-sleep path, but it returns a value rather than TimeoutException.
Add a construction probe for every framework path that creates a wait. Give it an always-false, side-effect-free condition, a clock that advances only through the recording sleeper, and a build-level timeout that can stop a frozen-loop mistake. The probe should assert that the supplied sleeper saw a request and that the controlled clock moved. It need not pin the exact number of evaluations unless the framework promises that count. This catches a factory that silently discards one injected dependency while leaving the policy tests apparently green.
The rollout order matters. Land the shared clock and sleeper fixtures first, followed by construction probes for the wait factories. Convert wait-loop unit tests only after those probes pass. Tests that hide wait construction inside page objects will fail first because they have no injection seam. Tests that pair a frozen clock with a sleeper that does not advance it can hang, so keep the focused job timeout in place during migration. After the policy tests are controlled, retain the existing browser lane and classify it explicitly as wall-clock integration coverage.
The change is working when policy tests show the same ordered evaluation and sleep schedule without launching a browser, while the integration lane still produces real browser artifacts for DOM and infrastructure failures. A shorter unit-test job is useful operationally, but it is not proof by itself. A test can become fast because it stopped exercising the wait. The recorded schedule and returned value are the proof.
Controlled time creates a specific maintenance burden. The framework now owns two kinds of evidence for a representative wait: a logical policy schedule and a browser integration result. Selenium upgrades require reviewing constructor wiring, exception behavior, and owned boundary assertions in the first set without weakening the second. Every diagnostic must label simulated durations so they cannot be copied into a production latency report. That duplication is the cost of keeping fast algorithm tests honest.
The automation-framework owner maintains the fixtures and wait factories. Feature teams own the predicate and the product timeout it represents. Browser-infrastructure owners investigate wall-clock runs, remote command delays, and runner cancellation. A handoff should contain the Selenium dependency version, factory path, controlled start and end, sleep request list, evaluation starts, exception cause, whether a browser was present, and separately labeled wall elapsed time. Without the factory path, the receiving team cannot tell whether the test exercised the injected constructor.
Clock and sleeper tests do not catch a semantically wrong predicate. A counter can return success on its third evaluation with a perfect schedule while the real condition watches the wrong job ID or accepts a hidden stale result. Keep a condition-level fixture that proves identity and state, plus the browser scenario that proves the rendered application uses that condition correctly.
Diagnose controlled-time tests that give misleading results
A fake-time test that never completes usually has a frozen clock. Print the clock instant and recorded requests from the fixture, then check that RecordingSleeper and FluentWait share the same MutableClock object. Also inspect whether the condition returns false forever before the sleeper can run.
An unexpected extra evaluation at the timeout boundary is not necessarily a bug. Read the FluentWait implementation for the pinned dependency and test the behavior the framework actually promises. A condition may be evaluated before the deadline check so a zero-timeout wait can still succeed immediately. Avoid deriving a universal poll-count formula from one version.
A missing sleep request can be correct. The first evaluation happens immediately, a successful evaluation ends the wait, an unignored exception ends it, and an interrupt from the sleeper prevents a normal return. Count sleeps and evaluations separately.
A timeout message without the expected cause may follow FluentWait's last-exception behavior. An ignored exception can be remembered, but a later false or null result clears it in the current implementation. Store domain observations in a condition when transitions matter; do not use a fake clock to paper over message design.
If a controlled test passes while the browser test remains flaky, the two tests cover different layers. Inspect actual DOM readiness, remote latency, application data, and Grid stability. Increasing fidelity in the unit test will not reproduce a race caused by a JavaScript component unless that component is included.
If a browser is present in the controlled-time test, be precise about what remains real. The sleeper may advance instantly while findElement() still blocks on an implicit wait or remote command. Logical timeout can jump around an operation that consumed wall time. That hybrid may be useful for a targeted experiment, but it is not a clean wait-loop unit test.
Avoid measuring test duration with System.nanoTime() and comparing it to fake-clock duration as though the values should match. They intentionally represent different timelines. Wall time can verify that the unit test stays quick, but use a generous build-level guard rather than a brittle millisecond assertion.
Timezone is rarely relevant to a duration-based wait, but a custom Clock must still implement it correctly. Use UTC in fixtures unless zone behavior is itself under test. Do not make daylight-saving transitions part of a polling test that only needs elapsed durations.
Introduce controlled time without weakening browser coverage
Start with wait utilities that currently consume real seconds in unit tests. Replace only their time fixture, keep their condition inputs fixed, and document which layer each test covers.
Create one shared pair of test classes such as MutableClock and RecordingSleeper, but instantiate them per method. Keep the fixture API small: advance, instant, and recorded requests. A feature-rich scheduler simulator invites unrelated production logic into the wait tests.
Add contract tests for immediate success, delayed success, timeout, ignored transient exception, unignored exception, and interrupted sleep. Do not assert every internal read of the Clock. Test observable policy so a harmless refactor does not break the suite.
Name test inputs as policy, not performance. timeoutOfOneSecond and pollEveryTwoHundredMillis tell a reader that the values configure a scenario. Avoid names such as fastGridLatency unless the suite collected and owns that measurement elsewhere. This small distinction prevents fake-clock numbers from leaking into capacity discussions.
Review the matrix for missing boundaries. Immediate success proves no initial sleep. Delayed success proves sleep between attempts. Timeout proves the message and exception. An ignored exception proves retry policy. An unignored exception proves fail-fast behavior. Interruption proves cancellation semantics. Condition-cost simulation proves the interval is not a poll-start guarantee. Together they cover the wait loop without pretending to render a page.
Retain real-browser tests for representative application waits. One should exercise a successful asynchronous transition. Another should preserve a terminal error or timeout diagnostic. These runs prove integration, not exact timing. Use actual artifacts from the run when discussing latency.
Keep the fast policy tests in a separate CI step so a browser setup problem cannot prevent them from running:
name: fluent-wait-contracts
on:
pull_request:
paths:
- "src/test/java/**"
- "pom.xml"
jobs:
wait-policy:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v4
- uses: actions/setup-java@v4
with:
distribution: temurin
java-version: "21"
cache: maven
- run: mvn -B -Dtest="Controlled*Test,*SleeperTest" test
- uses: actions/upload-artifact@v4
if: failure()
with:
name: fluent-wait-test-reports
path: target/surefire-reportsThe class pattern must match the project's test names. These tests should not require Chrome if they only pass strings or small domain objects as FluentWait input.
Pin Selenium through the build's dependency lock or dependency-management policy. Constructor and message behavior should be reviewed when upgrading. The tests provide a precise diff: requested sleeps, returned value, exception type, and message fragments.
If an upgrade changes an exact boundary count, read the release notes and current source before updating the assertion. Decide whether the team owns that count as a contract. If no caller depends on it, weaken the test to the observable result and sleep policy. If a rate-limited service depends on maximum attempts, keep the count and document why a Selenium change needs application review.
Publish failures from controlled tests as ordinary Surefire reports. Screenshots and browser traces add no value when no browser was launched. For the retained integration lane, capture the browser and application artifacts separately so reviewers can tell algorithm failure from environment failure at a glance.
Do not replace system time in the production wait factory just because tests can. An ordinary new FluentWait<>(input) uses Selenium's default Clock and Sleeper. WebDriverWait supplies WebDriver-specific defaults and ignores NotFoundException. Preserve those semantics unless the framework has a documented reason to change them.
What controlled time costs, and when not to use it
The fixture mirrors part of Selenium's loop. That makes tests fast, but it also creates maintenance when Selenium changes boundary behavior. Assert policies the team owns and leave implementation trivia unpinned unless exact behavior is required.
Logical time can create false confidence. A fake sleeper has no scheduler jitter, CPU contention, network latency, browser event loop, or remote command queue. It proves requested behavior under controlled inputs, not reliability under load.
Do not use it to select production timeouts. Those values should come from the product contract and observed environment, with headroom chosen deliberately. A unit test can verify that a configured value is passed to FluentWait; it cannot justify the value.
Do not turn MutableClock into a global test service. Tests that set or advance shared logical time become order-dependent, and parallel workers can make the schedule impossible to reconstruct. Constructor injection is slightly more verbose, but it keeps ownership visible.
Avoid asserting wall-clock completion under an extremely tight limit. CI scheduling, class loading, and garbage collection are real even when logical sleep is instant. A broad build timeout can catch an accidental infinite loop; a millisecond stopwatch assertion creates noise.
Avoid controlled time for a straightforward end-to-end wait. Injecting test clocks through page objects and application flows increases complexity and can diverge from production wiring. Keep the browser test on the real wait and use focused fixtures for the helper.
Do not mock the condition and then claim the page transition is tested. A counter that succeeds on the third call proves poll sequencing only. Pair it with a separate condition test or browser scenario.
Skip exact poll-count assertions when callers care only about eventual success. Counts are sensitive to deadline comparison and evaluation cost. Assert result, exception policy, and message instead.
Never use a sleeper that advances time but hides an interrupt. Cancellation is part of worker safety. The interruption test protects the thread flag and wrapped cause, while higher-level executor tests protect the build's cancellation path.
Finally, keep one honest slow path. A small real-browser smoke test can reveal implicit waits, Grid delays, application churn, and artifact gaps that controlled time cannot see. The fast tests protect the wait algorithm; the integration test protects the system that uses it.
// FIELD DISPATCH
Get the QA Field Notes
Weekly QA battles, AI testing guides, and interview drills. Free on Substack.
// LIVE COURSE / THE TESTING ACADEMY
Playwright Automation Mastery
Go beyond Selenium. Master Playwright with JS/TS in 90 days.
From the instructor behind this guide.
Playwright jobs are growing 8x faster than Selenium. 90 days / 75+ live hrs / Tue-Thu-Sat 7 AM IST.
PRIMARY REFERENCES
Verify the details at the source
QABattle guides are practical explanations. Product behavior, standards, and APIs can change, so use these primary references for the canonical details.
- 01Official selenium.dev reference
selenium.dev
Primary documentation selected and verified for the claims in this guide.
- 02Official selenium.dev reference
selenium.dev
Primary documentation selected and verified for the claims in this guide.
- 03Official selenium.dev reference
selenium.dev
Primary documentation selected and verified for the claims in this guide.
- 04Official selenium.dev reference
selenium.dev
Primary documentation selected and verified for the claims in this guide.
FAQ / QUICK ANSWERS
Questions testers ask
Why inject a Clock and Sleeper into FluentWait?
They let a unit test control timeout measurement and record requested polling sleeps without spending real time. This is useful for wait-policy code, but it does not measure browser or Grid performance.
Which FluentWait constructor accepts controlled time?
Use FluentWait(T input, Clock clock, Sleeper sleeper), then configure withTimeout() and pollingEvery(). WebDriverWait also exposes a constructor with timeout, sleep duration, Clock, and Sleeper when WebDriver-specific behavior is required.
Why does my fake-time FluentWait loop forever?
The supplied clock must advance when the sleeper receives a duration, or during condition evaluation if that is what the test models. An always-false condition with a frozen clock can never cross the timeout.
Does pollingEvery() guarantee the time between poll starts?
No. Selenium documents the interval as the delay between evaluations, and condition execution time is not included in it. A slow condition makes the distance between poll starts larger than the requested sleep.
Should every Selenium wait use a fake clock in production?
Keep production waits on the normal system clock and sleeper unless the application has an unusual, reviewed requirement. Controlled time belongs mainly in focused tests of wait helpers and policy.
RELATED GUIDES
Continue the learning route
GUIDE 01
Data-Driven Testing in Selenium
Learn data-driven testing in Selenium using CSV, Excel, and TestNG DataProvider patterns to run one script across many reusable test datasets.
GUIDE 02
Add Selenium Accessibility Testing to User Journeys
Master Selenium accessibility testing with practical examples, architecture decisions, failure analysis, CI guidance, metrics, and scenario-led interview answers.
GUIDE 03
Debug ElementClickInterceptedException with Overlays and Hit Testing
Debug Selenium click interception by inspecting the center-point hit test, overlay lifecycle, scrolling geometry, and application readiness before retrying.
GUIDE 04
Java Reflection and Annotations for Custom Test Runners
Java reflection annotations test runners: practical design, implementation, debugging, CI, metrics, and interview guidance for QA, SDET, and automation engineers.
GUIDE 05
Rest Assured Tutorial: API Testing with Java from Scratch
Rest Assured tutorial for Java testers covering setup, requests, assertions, auth, JSON validation, framework design, and common mistakes.