advanced20 min

JUnit Integration and Parameterized Tests

Structuring a Selenium suite with JUnit 5's assertions and lifecycle properly, and running the same test logic across many inputs with @ParameterizedTest.

What you'll learn

  • Use JUnit 5 assertions correctly to verify Selenium-driven browser state
  • Write a @ParameterizedTest covering several input values with one shared test body
  • Explain why parameterized tests report failures more usefully than a hand-written loop inside one test

Prerequisites

Explanation

A Selenium test class built on JUnit 5 (Jupiter) combines this course's driver-lifecycle pattern (@BeforeEach/@AfterEach) with JUnit's standard assertions: assertEquals(expected, actual), assertTrue(condition), assertThrows(SomeException.class, () -> ...). Applied to Selenium specifically, a genuinely useful assertion checks something a real user would actually perceiveassertTrue(driver.findElement(By.id("welcome")).isDisplayed()), assertEquals("Dashboard", driver.getTitle()) — rather than an internal implementation detail that happens to be technically true but doesn't reflect real user-facing correctness.

@ParameterizedTest runs the same test body once per supplied value, exactly the same principle behind parameterized testing in any framework: @ParameterizedTest @ValueSource(strings = {"learner", "instructor", "admin"}) void dashboardShowsCorrectRoleLabel(String role) { ... } generates three distinct, individually-reportable test executions from one method body — a real failure specifically in the "admin" case is reported as dashboardShowsCorrectRoleLabel[3] (or with the actual value shown, depending on configuration), not folded into one ambiguous failure covering all three roles at once. @CsvSource, @MethodSource, and @EnumSource provide richer parameter shapes — multiple parameters per invocation, values computed by a method, or every value of an enum — for cases a plain @ValueSource string/int list can't express.

The concrete, honest reason a hand-written loop inside one test method (for (String role : roles) { ... assertEquals(...) ... }) is worse than @ParameterizedTest, stated precisely: a loop's first failing assertion stops the entire test method immediately (an assertion failure throws), so if the "learner" case fails, you never learn whether "instructor" or "admin" would have passed or failed too — you have to fix the first failure and re-run just to find out about the rest. @ParameterizedTest runs and reports every value's test independently, regardless of whether an earlier one failed, giving you the complete picture — every case's actual pass/fail status — from a single run, not one piece of it at a time across repeated fix-and-rerun cycles.

Example

Modeling the loop-stops-at-first-failure problem versus independently-reported parameterized results, as data.

function runAsLoop(values, checkFn) {
  for (const v of values) {
    const result = checkFn(v);
    if (!result.passed) {
      return { stoppedAt: v, remainingUnknown: values.slice(values.indexOf(v) + 1) };
    }
  }
  return { allPassed: true };
}

function runAsParameterized(values, checkFn) {
  return values.map((v) => ({ value: v, ...checkFn(v) })); // every value's result, independently
}

function check(role) {
  return { passed: role !== "instructor" }; // simulate "instructor" being the one broken case
}

console.log(runAsLoop(["learner", "instructor", "admin"], check));
// { stoppedAt: "instructor", remainingUnknown: ["admin"] } -- "admin" was NEVER actually checked

console.log(runAsParameterized(["learner", "instructor", "admin"], check));
// full, independent results for ALL THREE -- including confirming "admin" genuinely passed

Try it yourself

Change check() so 'admin' is the broken case instead, and compare what runAsLoop reveals about 'instructor' in each scenario.

Code editor. Press Escape then Tab to leave the editor if keyboard focus becomes trapped. Press Control+Shift+M inside the editor to toggle Tab-key focus trapping.

Loading editor…

Guided exercise

Guided exercise

Write runAsParameterized(values, checkFn) that returns an array with ONE result object per value ({value, passed}), regardless of whether earlier values failed -- modeling @ParameterizedTest's independent-reporting behavior.

Checks: reports a result for every value regardless of earlier failures · confirms a later value's genuine pass/fail status independently

Code editor. Press Escape then Tab to leave the editor if keyboard focus becomes trapped. Press Control+Shift+M inside the editor to toggle Tab-key focus trapping.

Loading editor…

Stuck? Get a hint.

Independent exercise

Independent exercise

Write summarizeParameterizedResults(results) where results is an array of {value, passed} objects. Return {passedCount, failedValues} -- the count of passing results, and an array of just the VALUES (not the whole objects) that failed.

Checks: correctly summarizes a mix of passing and failing results · handles an empty results array

Code editor. Press Escape then Tab to leave the editor if keyboard focus becomes trapped. Press Control+Shift+M inside the editor to toggle Tab-key focus trapping.

Loading editor…

Stuck? Get a hint.

Common mistakes

  • Writing a hand-rolled for-loop with assertions inside one test method instead of using @ParameterizedTest -- the loop stops at the FIRST failing assertion, hiding whether later values would have passed or failed too, requiring repeated fix-and-rerun cycles to find out.
  • Asserting on an internal implementation detail (a hidden attribute value, an internal state flag) instead of something a real user would actually perceive (visible text, an element's displayed state) -- this can pass while the actual user-facing behavior is broken.
  • Using @ValueSource for parameter shapes it can't express (multiple related parameters per case) instead of reaching for @CsvSource or @MethodSource when the data genuinely needs more structure.

Knowledge check

Knowledge check

1. A hand-written loop asserts on three values inside one test method; the first value fails. What happens to the other two?
2. How does @ParameterizedTest handle a failure in one of several supplied values, compared to a hand-written loop?
3. Why is `assertTrue(driver.findElement(By.id("welcome")).isDisplayed())` a better assertion than checking an internal, non-user-facing implementation detail?

Takeaway

@ParameterizedTest runs and reports every supplied value's test independently, unlike a hand-written loop that stops entirely at the first failing assertion — and a Selenium assertion is most valuable when it checks something a real user would actually perceive, not an internal implementation detail.

Summary

JUnit 5 assertions (assertEquals, assertTrue, assertThrows) should verify real, user-perceivable state through Selenium. @ParameterizedTest (with @ValueSource, @CsvSource, or @MethodSource) generates independent, individually-reported test executions per value, unlike a hand-written loop that stops at the first failure and hides the status of remaining cases.

References

Your notes

Notes save automatically.