Chapter 27Lesson 05~255 minutes

Checkpoint Lab — Framework Integration: pytest, JUnit, TestNG, NUnit, and Language Bindings

Prove the same browser behavior with two runnable bindings, compare lifecycle and API differences, and define a team rule that keeps framework mechanics separate from browser semantics.

CheckpointCross-bindingEvidenceTeam ruleCI readiness

Learning objectives

  • Run equivalent Python/pytest and JavaScript/Mocha tests against one loopback behavior contract.
  • Predict and verify session, DOM, AUT, and evidence changes in each binding.
  • Build a cross-binding framework matrix with version and lifecycle assumptions.
  • Produce evidence showing the two tests assert the same business behavior.
  • Define a team rule that prevents framework mechanics from fragmenting browser semantics.

1. Checkpoint scenario and exact assumptions

You will run the same save synthetic name contract with Python/pytest and JavaScript/Mocha against the loopback fixture. The Python path requires Python 3.10+, Selenium 4.47.0, and pytest 9.1.1. The JavaScript path requires Node.js 22+, selenium-webdriver@4.47.0, and Mocha 11.8.0. A locally installed supported Chrome/Chromium-family browser is required; Selenium Manager handles normal driver resolution.

Scope

No paid Grid, production URL, real login, proxy, certificate change, or external test account is part of this checkpoint. If a second language runtime is unavailable, inspect its code and run the provided simulation/evidence comparison; do not substitute a public target.

2. Setup and preflight

The following example makes the Setup and preflight behavior concrete. Read it with the stated assumptions, then compare its observable output or state changes with the explanation that follows.

python -m venv .venv
# activate the environment for your shell
python -m pip install selenium==4.47.0 pytest==9.1.1
npm init -y
npm install --save-dev selenium-webdriver@4.47.0 mocha@11.8.0
python make_framework_fixture.py
python -m http.server 8777 --bind 127.0.0.1 --directory framework-lab

Before opening a browser, request http://127.0.0.1:8777/ with a normal HTTP client and confirm the title/status fixture is present. Record python --version, node --version, package versions, and the chosen browser version in the evidence packet.

3. Prediction gate

Write predictions before running:

  1. The Python and JavaScript tests will create different session IDs even though they target the same browser type and URL.
  2. Both tests will mutate the same kind of DOM state from idle to saved:Ada, then teardown will close each independent session.
  3. The evidence directories will differ by binding while the final actual business value will match exactly.

4. Runnable Python binding test

The following example makes the Runnable Python binding test behavior concrete. Read it with the stated assumptions, then compare its observable output or state changes with the explanation that follows.

# file: test_checkpoint_python.py
import json, os
from pathlib import Path
import pytest
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC

BASE_URL = os.getenv("BASE_URL", "http://127.0.0.1:8777/")

@pytest.fixture
def driver():
    d = webdriver.Chrome()
    try:
        yield d
    finally:
        d.quit()

def test_same_browser_contract(driver):
    out = Path("evidence/python"); out.mkdir(parents=True, exist_ok=True)
    driver.get(BASE_URL)
    driver.find_element(By.CSS_SELECTOR, '[data-testid="name"]').send_keys("Ada")
    driver.find_element(By.CSS_SELECTOR, '[data-testid="save"]').click()
    WebDriverWait(driver, 5).until(
        EC.text_to_be_present_in_element((By.CSS_SELECTOR, '[data-testid="status"]'), "saved:Ada")
    )
    actual = driver.find_element(By.CSS_SELECTOR, '[data-testid="status"]').text
    driver.save_screenshot(str(out / "state.png"))
    (out / "run.json").write_text(json.dumps({
        "binding": "python", "framework": "pytest", "selenium": "4.47.0",
        "session_id": driver.session_id, "browser": driver.capabilities.get("browserName"),
        "browserVersion": driver.capabilities.get("browserVersion"),
        "url": driver.current_url, "actual": actual,
    }, indent=2), encoding="utf-8")
    assert actual == "saved:Ada"

This is a real pytest test: discovery, fixture setup/teardown, assertion reporting, and browser evidence are all exercised together. Evidence is written before the final assertion so an intentionally changed expectation still preserves the observed browser state.

5. Runnable JavaScript binding test

The following example makes the Runnable JavaScript binding test behavior concrete. Read it with the stated assumptions, then compare its observable output or state changes with the explanation that follows.

// file: test_checkpoint_javascript.js
const fs = require('node:fs');
const path = require('node:path');
const assert = require('node:assert/strict');
const {Builder, By} = require('selenium-webdriver');

describe('same browser contract', function () {
  this.timeout(15000);
  let driver;
  const base = process.env.BASE_URL || 'http://127.0.0.1:8777/';

  beforeEach(async function () {
    driver = await new Builder().forBrowser('chrome').build();
  });

  afterEach(async function () {
    if (driver) await driver.quit();
  });

  it('saves Ada', async function () {
    const out = path.join('evidence', 'javascript');
    fs.mkdirSync(out, {recursive: true});
    await driver.get(base);
    await driver.findElement(By.css('[data-testid="name"]')).sendKeys('Ada');
    await driver.findElement(By.css('[data-testid="save"]')).click();
    const status = await driver.findElement(By.css('[data-testid="status"]'));
    await driver.wait(async () => (await status.getText()) === 'saved:Ada', 5000);
    const actual = await status.getText();
    await driver.takeScreenshot().then(data => fs.writeFileSync(path.join(out, 'state.png'), data, 'base64'));
    const caps = await driver.getCapabilities();
    const session = await driver.getSession();
    fs.writeFileSync(path.join(out, 'run.json'), JSON.stringify({
      binding: 'javascript', framework: 'mocha', selenium: '4.47.0', session_id: session.getId(),
      browser: caps.get('browserName'), browserVersion: caps.get('browserVersion'),
      url: await driver.getCurrentUrl(), actual,
    }, null, 2));
    assert.equal(actual, 'saved:Ada');
  });
});

This is a real Mocha test. Its async hooks own the session, every relevant WebDriver Promise is awaited, and the evidence bundle is written before the assertion so a red test still preserves the observed state.

6. Run commands and evidence packet

The following example makes the Run commands and evidence packet behavior concrete. Read it with the stated assumptions, then compare its observable output or state changes with the explanation that follows.

# terminal 1: fixture server is already running
# terminal 2:
pytest -q test_checkpoint_python.py
npx mocha test_checkpoint_javascript.js
pytest -q test_framework_contract.py
npx mocha test_framework_contract.js

# inspect evidence
type evidence\python\run.json      # Windows PowerShell/cmd alternative: Get-Content
# or on POSIX: cat evidence/python/run.json
# inspect evidence/javascript/run.json similarly

Keep the evidence files separate. Never let the second binding overwrite the first binding’s screenshot, JSON, or logs. If you run them concurrently later, include worker/run IDs in paths as established in Chapter 21.

7. Cross-binding comparison matrix

The following table organizes the key choices and evidence for Cross-binding comparison matrix. Use it together with the surrounding prose so the rows serve as a comparison aid rather than standalone rules.

Stack Discovery/lifecycle Parameterization Async model Current baseline
Python + pytest fixture/yield; test_* discovery @pytest.mark.parametrize blocking-looking WebDriver calls Selenium 4.47.0; pytest 9.1.1
Java + JUnit @BeforeEach/@AfterEach @ParameterizedTest blocking-looking calls Selenium 4.47.0; JUnit 6.1.3; Java 17+
Java + TestNG @BeforeMethod/@AfterMethod @DataProvider blocking-looking calls Selenium 4.47.0; TestNG 7.9.0
.NET + NUnit [SetUp]/[TearDown] [TestCase] blocking-looking calls in shown API Selenium 4.47.0; NUnit 4.6.1
JavaScript + Mocha beforeEach/afterEach generated it() cases Promise/await selenium-webdriver 4.47.0; Mocha 11.8.0; Node 22+

8. Verify equivalence independently

  • Both evidence JSON files report actual: saved:Ada.
  • Session IDs are present and different between runs.
  • Browser name/version are recorded rather than assumed from the source code.
  • The final URL is loopback-only.
  • Both processes exit successfully only after the assertion passes and teardown completes.
  • No credential, personal data, or public Grid address appears in artifacts.

9. Failure injection: prove framework mechanics do not change the diagnosis

Temporarily change the expected string in one test to saved:Alan. The browser should still show saved:Ada; the failure must be classified as an assertion/test-expectation failure. Then restore the expectation. Next, stop the fixture server and rerun one test: that is an environment/network reachability failure, not the same category.

10. Team rule: keep browser semantics separate from framework mechanics

Rule: Every language/framework implementation must preserve the canonical browser contract—stable locators, explicit readiness, one independently owned session per concurrent test, synthetic isolated data, business assertions in the test layer, first-failure evidence, and failure-safe cleanup. Framework annotations, fixtures, plugins, and async syntax may differ, but they may not silently change those semantics.

11. Cleanup and rollback

  1. Stop the loopback HTTP server.
  2. Verify both WebDriver sessions are closed.
  3. Remove framework-lab/, evidence/, node_modules/ if created only for the lab, and the disposable virtual environment if desired.
  4. Restore any deliberately changed expected value.
  5. Keep only a redacted comparison report if you want a learning artifact.

12. Production operating model and bridge to Chapter 28

Chapter 27 adds a framework/binding portability layer to the operating model: lifecycle ownership, parameterization, assertion semantics, async discipline, parallel scheduling, and reports are now explicit. Chapter 28 uses that foundation to debug stale elements, timing races, driver failures, and Grid incidents without letting framework wrappers hide the original failure.

Next chapter

Debugging Stale Elements, Timing Races, Driver Failures, and Grid Incidents: Core Concepts and Mental Model

Continue with Debugging Stale Elements, Timing Races, Driver Failures, and Grid Incidents: Core Concepts and Mental Model. It builds directly on the state, evidence, and operating assumptions established here, so carry those constraints forward rather than treating the next page as an isolated topic.

Official references and current-version notes

Version and compatibility note

Version-sensitive statements in this lesson retain the pinned baseline used when the lesson was authored. Before changing Selenium, browser, driver, Grid, BiDi, container, or framework dependencies, compare that baseline with current primary documentation instead of silently substituting an unverified “latest” environment.

Knowledge checks

What proves Python and JavaScript tests are behaviorally equivalent?

Should the two bindings produce the same session ID?

If the expected text is deliberately changed to saved:Alan, what category should fail?

What does stopping the local fixture server test?

What must remain invariant when teams choose different frameworks?

Summary and next bridge

This lesson keeps test-framework mechanics subordinate to the WebDriver/browser contract: lifecycle, assertions, parameters, async behavior, scheduling, and reports remain explicit rather than hiding session ownership or failure meaning.

Next: Chapter 28 — Debugging Stale Elements, Timing Races, Driver Failures, and Grid Incidents

Keep the academy open

Support free, practical DevOps education.

Every lesson is designed to remain readable in a browser, downloadable from GitHub, and usable without a paid learning platform. Contributions help expand and maintain the curriculum.

Ethereum / ERC-20
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0Send only Ethereum/ERC-20 compatible assets to this address.