ScreenshotNeo

BlogHow-to

Using Selenium and Python Hypothesis for Automated Testing

Combine Selenium WebDriver with Hypothesis to test browser behavior across generated inputs and action sequences, with waits, isolation, failure replay, and practical examples.

By the ScreenshotNeo team4 October 202610 min read

Use Selenium WebDriver to operate the browser and Hypothesis to generate inputs or sequences of user actions. Use Hypothesis’s @given decorator when a property should hold across many independent inputs. Use a state machine when the order of actions matters. For dynamic pages, wait for the condition the test needs, and give every generated example isolated application state.

The Selenium and Hypothesis documentation describes each tool separately. The combined examples here are an editorial synthesis; they are patterns to adapt, not an officially documented integration or code tested against a particular application.

1. Install the tools and choose a test target

The current Selenium Python API documentation lists Python 3.10 or later. It documents support for Chrome, Edge, Firefox, Safari, WebKitGTK, WPEWebKit, and remote protocol use. Check the current API documentation for version and environment details before setting up a project. Modern Selenium uses Selenium Manager to handle browser and driver setup on most supported platforms; manual configuration is also possible.

python -m pip install -U selenium hypothesis pytest

Save dependencies in your project’s normal dependency or lock file so local runs and CI use the same versions. The example assumes a controlled application at https://example.test that has a search field named q and renders an element with ID search-results. Replace those with real selectors and a meaningful expected behavior in your app.

For Selenium’s Python API and setup details, see the Selenium Python API documentation. Hypothesis installation and its generated-test model are covered in the Hypothesis quickstart.

2. Start with an ordinary Selenium test

Before adding generated cases, make sure one deterministic browser test expresses the behavior you care about. The following is a pytest-shaped example; the fixture named driver must be supplied by your project and own browser startup and teardown.

from selenium.webdriver.common.by import By
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.support.ui import WebDriverWait


def test_search_page_shows_results(driver):
    driver.get("https://example.test/search")

    field = driver.find_element(By.NAME, "q")
    field.clear()
    field.send_keys("selenium")
    field.submit()

    results = WebDriverWait(driver, 10).until(
        EC.visibility_of_element_located((By.ID, "search-results"))
    )
    assert results.is_displayed()

This checks one chosen input. Hypothesis is useful when the expected property should hold across a meaningful range of values, rather than only for a few examples selected by hand.

3. Use @given for independent generated inputs

Hypothesis strategies describe the values a test can receive. The @given decorator runs the test with generated values; ordinary generated tests are regular Python functions and can be used with pytest or unittest.

from hypothesis import given, strategies as st
from selenium.webdriver.common.by import By
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.support.ui import WebDriverWait


@given(st.text(min_size=1, max_size=40))
def test_search_input_is_accepted(driver, search_term):
    driver.get("https://example.test/search")

    field = driver.find_element(By.NAME, "q")
    field.clear()
    field.send_keys(search_term)
    field.submit()

    results = WebDriverWait(driver, 10).until(
        EC.visibility_of_element_located((By.ID, "search-results"))
    )
    assert results.is_displayed()

The strategy above generates non-empty text up to 40 characters. That does not mean every generated string is valid for every application. Constrain strategies to the app’s accepted input rules, or express expected validation behavior for invalid values too. If the application has a maximum length, allowed character set, or server-side validation, model those explicitly.

Hypothesis quickstart documents 100 generated inputs as the default and a max_examples setting to adjust the count. More examples can increase the chance of finding edge cases, but each browser launch, navigation, and server interaction costs time. Start with a useful bounded strategy and tune the example count to the test’s runtime and value.

Isolation is essential. Each generated example should start from a known state. Reset the app, use unique records, clear the relevant data, or create a fresh test account as appropriate. Otherwise one generated example may inherit browser or server state from a previous example and produce misleading failures. Neither tool guarantees application-state isolation for you.

4. Synchronize on page conditions, not fixed delays

Modern pages often change after the initial HTML loads. A browser can reach a command before or after a JavaScript-driven update, creating a timing race. Selenium recommends explicit waits for the condition the test requires, such as visibility or clickability. A fixed time.sleep() can waste time when the page is ready early and still be too short when it is slow.

from selenium.webdriver.common.by import By
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.support.ui import WebDriverWait

wait = WebDriverWait(driver, 10)
button = wait.until(
    EC.element_to_be_clickable((By.CSS_SELECTOR, "button[type='submit']"))
)
button.click()
wait.until(
    EC.visibility_of_element_located((By.ID, "confirmation"))
)

Choose a wait condition that matches the next action or assertion. For example, wait for visibility before reading visible text, or for clickability before clicking. Selenium warns that mixing implicit and explicit waits can produce unpredictable total wait times. Prefer explicit waits for these condition-based tests and avoid casually combining the two strategies.

See Selenium’s waiting strategies documentation for wait behavior and examples.

5. Use a state machine when action order matters

Use @given when test cases are meaningfully independent. If the browser’s state changes after each action, a Hypothesis RuleBasedStateMachine can generate sequences of rules as well as their values. A useful model might include actions such as adding an item, removing an item, and submitting a form, with an invariant checked after each step.

Keep the expected model small and understandable. For a cart, for example, the model might be a Python list of item identifiers; after each browser operation, assert that the visible cart matches the model. This is an editorial modeling pattern, not a property provided automatically by Selenium or Hypothesis.

from hypothesis.stateful import RuleBasedStateMachine, invariant, rule


class CartMachine(RuleBasedStateMachine):
    def __init__(self):
        super().__init__()
        # Project-specific setup belongs here:
        # create or reset a test cart and open the browser page.
        self.model_items = []

    @rule(item_id="item-1")
    def add_item(self, item_id):
        # Replace with Selenium actions for the controlled application.
        # For example: find the item's add button and click it.
        self.model_items.append(item_id)

    @rule()
    def remove_last_item(self):
        if self.model_items:
            # Replace with the matching browser action in the real app.
            self.model_items.pop()

    @invariant()
    def visible_cart_matches_model(self):
        # Replace with a Selenium read and assertion against model_items.
        assert isinstance(self.model_items, list)


TestCart = CartMachine.TestCase

This skeleton intentionally leaves application-specific browser actions and assertions as replacements: without the target app’s selectors and semantics, runnable UI actions cannot be specified honestly. Hypothesis describes rules as chained operations and invariants as checks run after steps. Its documentation also points out that simpler behavior may be better tested with ordinary @given tests. Use a state machine when sequences and state transitions are central to the bug or requirement.

Read the Hypothesis stateful testing guide for rule, invariant, and failure-sequence details.

6. Run generated browser tests in pytest

Run a module or a specific test with pytest as usual:

python -m pytest -q tests/test_search.py

Hypothesis integrates with ordinary pytest tests. Browser lifecycle belongs in a pytest fixture or equivalent framework lifecycle so each test owns a well-defined browser session. The exact fixture depends on your project’s browser, CI environment, and parallelism strategy.

Keep generated examples isolated at the application level as well as the browser level. A new WebDriver session does not necessarily reset server-side records, queued jobs, rate limits, or shared test data. Prefer a dedicated test environment and deterministic setup/cleanup.

7. Diagnose and replay a failure

Hypothesis shrinks failing cases to try to find a simpler input or action sequence. Stateful failures can be reported as a short, program-like sequence of actions. Preserve the minimized reproducer in the defect report: it can show the smallest sequence that still violates the property.

Hypothesis supports seeds, including pytest’s --hypothesis-seed option, which can help replay generated cases:

python -m pytest -q tests/test_search.py --hypothesis-seed=12345

Seed replay is not a promise that every browser failure will repeat identically. Timing, external services, application data, browser versions, and other nondeterministic influences can change the outcome. Hypothesis documents this limitation and distinguishes seed replay from deterministic CI behavior. Record the seed and the minimized example, then also capture the relevant app state, browser version, and logs when investigating a flaky failure.

See Hypothesis’s settings and reproducibility documentation.

8. Choose a test shape that fits the property

Question Use @given Use a state machine
What varies most? Independent input values Action order and accumulated state
What should one case do? Set up, exercise one input, check a property Apply multiple rules and check invariants along the way
What must the model track? Usually just the current generated input and expected result A small representation of state, such as cart contents
What is the main cost? Browser setup and app work per generated example Browser work across generated action sequences
What should a failure explain? A minimized input example A minimized sequence of actions

Runtime and model clarity are practical design considerations, not quantified performance claims. Browser-driven generated tests can be expensive because each example performs real browser and application work. Keep the property focused, avoid generating values the application cannot meaningfully accept unless validation is under test, and reserve longer or broader runs for an appropriate CI stage.

9. Troubleshooting Selenium and Hypothesis tests

Symptom Likely cause Fix
Element not found The page has not rendered the element yet, or the selector does not match this page. Verify the selector in the controlled app and wait for the relevant presence or visibility condition.
Click or assertion is intermittently early A JavaScript update or navigation has not reached the needed state. Wait explicitly for the next action’s condition, such as clickability, visibility, or a URL change.
Waits take much longer than expected Implicit and explicit waits may be mixed, or the condition never becomes true. Use a consistent wait approach, avoid casually mixing wait types, and inspect the condition and timeout.
Generated examples fail after earlier examples pass Application or server-side state leaks between generated cases. Reset data or use isolated records and cleanup for each case.
Hypothesis finds values the app rejects The strategy is broader than the product’s accepted input domain. Constrain the strategy to valid inputs or write explicit assertions for validation behavior.
Failure does not replay with the same seed Timing, external state, browser changes, or other nondeterminism differs. Preserve the minimized reproducer and seed, stabilize the environment, and capture app and browser state.
Browser or driver does not start Browser installation, platform support, permissions, or environment configuration is missing or incompatible. Check current Selenium setup documentation and browser availability; use Selenium Manager where supported or configure the browser and driver explicitly.
Generated suite is too slow Each example incurs navigation, browser, and server work. Focus the property, reduce redundant setup, tune example counts deliberately, and separate quick feedback runs from broader runs.

10. Or skip the browser setup

If your goal is to capture pages for visual review or documentation rather than exercise interactions, ScreenshotNeo offers a website screenshot API and MCP server. It is not a replacement for Selenium interaction tests: it returns screenshots or PDFs from a URL. Its cookie and consent handling accepts banners like a visitor and removes more than 60 known consent platforms, newsletter popups, and chat widgets before capture; each of those steps can be turned off. Bot checks, blank pages, failed loads, timeouts, and cache hits cost nothing, and response headers report the page verdict and billing status. An MCP server provides take_screenshot, get_page_info, and capture_pdf tools for AI agents using Claude, Cursor, or another MCP client.

One GET request captures a page. See the ScreenshotNeo API documentation for options and response details.

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://example.com -o shot.webp
import requests

r = requests.get(
    "https://api.screenshotneo.com/v1/shot",
    params={"access_key": "YOUR_API_KEY", "url": "https://example.com"},
    timeout=90,
)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://example.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`Screenshot request failed: ${res.status}`);
await Bun.write('shot.webp', res);

ScreenshotNeo supports PNG, JPEG, WebP, or PDF; full-page and element capture; device and viewport settings; custom CSS and JavaScript; waits; request blocking; headers and cookies; caching; async jobs; bulk capture; and other options. It has a free plan with 1,000 screenshots per month and no card; paid plans start at $5 for 3,000 screenshots. Every feature is on every plan. For browser tests, continue using Selenium and Hypothesis; for URL-based screenshots, sign up free for 1,000 screenshots a month, with no card.

11. Frequently asked questions

How do I use Hypothesis with Selenium in Python?

Decorate a pytest-compatible function with @given, pass a strategy-generated value into the function, perform browser actions with Selenium, and assert a property that should hold for that value. Reset application state for each generated example.

How do I test dynamic pages without time.sleep()?

Use WebDriverWait with an expected condition tied to what the next action or assertion needs, such as visibility or clickability.

When should I use a Hypothesis state machine?

Use one when a meaningful test depends on a sequence of state-changing actions. For independent inputs and outcomes, a regular @given test is usually simpler.

How can I reproduce a flaky Selenium test?

Keep Hypothesis’s minimized failing example or action sequence and its seed, then stabilize and record the application state, browser environment, and timing conditions. A seed alone cannot eliminate other sources of nondeterminism.