1. Imperative vs Declarative Tests (and Why It Matters)
Titus starts by showing:
Imperative test:
Raw Selenium API calls
Locators embedded in the test
String literals for email/password
Declarative test:
signInPage.visit()signInPage.signIn(user)navBar.isLoggedInAs(user)
What to notice:
The behavior of the test doesn’t change—its readability and flexibility do.
Declarative tests:
Survive UI changes better (page object shields them).
Are much easier for new team members (or AI tools) to understand at a glance.
As you watch, ask:
“If I read only the test method names and arguments, would I immediately know what behavior is being validated?”
2. Why “Classic” Data-Driven Testing Often Hurts
Titus deliberately critiques common patterns:
Long method signatures with many ordered parameters (10–12 arguments).
“Data-driven” test frameworks that:
Repeat the same test multiple times,
Hide meaning in CSV/Excel rows,
Depend on strict, brittle argument ordering.
Key takeaway:
These patterns are often more clever than useful.
They sacrifice clarity and semantics:
You can’t quickly see what each row is testing.
It’s harder to debug when things go wrong.
If you’ve ever stared at a big test data sheet and thought, “I have no idea what scenario row 27 actually represents,” this section will feel uncomfortably familiar—and that’s the point.
3. Data Objects 101: From Strings to Meaningful Users
Titus introduces a simple User data object:
Private fields:
email,passwordConstructor + getters (and later setters)
Used in tests like:
User user = new User(email, password);signInPage.signIn(user);navBar.isLoggedInAs(user);
Early on, this looks like a 1:1 replacement for passing strings. Titus calls this out: if all you do is wrap strings, you didn’t actually gain much.
The real value comes when you start encoding intent:
User.valid()– a clearly named method that returns a valid user.Tests now read like:
User user = User.valid();Immediately explains what kind of user the test is using.
This is where data objects become semantic, not just structural.
4. Just-In-Time Test Data & Parallel Execution
Titus references four approaches to test data management and emphasizes one:
The only way to safely scale tests in parallel is for each test to own its own data.
That’s the just-in-time approach:
Each test:
Creates its own data,
Uses it,
Doesn’t depend on shared records or preloaded fixtures.
Why this matters:
Greatly reduces flakiness from data collisions.
Makes parallel runs safe (no two tests fight over the same user/order/cart).
Fits how modern CI and cloud grids work.
When he moves from sign in (existing user) to sign up (must be new each time), this principle becomes very concrete.
5. Faker + Random-by-Default Constructors
The big “aha” moment:
Instead of manually calling
User.random()everywhere, Titus makes the default constructor itself random.Fields like
emailandpasswordare initialized using Faker:Faker.internet().emailAddress()Faker.internet().password()
What this gives you:
new User()→ always a valid, random user.Tests become incredibly concise:
User user = new User();No spreadsheets, no hard-coded junk you don’t care about.
The huge “data sets” people maintain manually become mostly unnecessary.
This pattern is particularly helpful if:
You mostly care that data exists (not which value).
You want robustness against “test knows too much about the exact value” issues.
6. Overriding Only What Matters (Targeted Scenarios)
Once you have random defaults, Titus shows how to express specific scenarios:
Use setters or helper methods to override only the fields that matter:
User user = new User();user.setPassword("");(blank password scenario)
Or via a helper:
User user = User.withBlankPassword();Under the hood:
Create a default user,
Override
passwordfield.
Why this is powerful:
You get clear scenario names (e.g., “blank password”, “Alaska shipping address”) instead of cryptic rows.
Data you don’t care about stays random and out of your way.
This scales to big forms:
e.g., testing “must send to Alaska”:
Override
state,city,zip,countryonly.Let everything else (name, secondary address, etc.) be random.
7. Reflection Magic: Auto-Wiring Data Objects to Page Objects
The advanced part of the talk is where Titus leans into Java reflection and wrapped element classes.
Core idea:
Page objects and data objects share field names (
email,password).WebElements are wrapped in custom types:
BrowserTextFieldBrowserElement
Base page class:
Reflectively inspects its fields to identify “element” fields.
Base data class:
Reflectively retrieves values by field name.
Then he wires it all together:
fillForm(dataObject) in Base Page
Loops over fields in the page object.
For each text field:
Looks up matching value from data object using reflection.
Calls
sendKeyson that field.
submitForm(dataObject)
Calls
fillForm(dataObject).Clicks whatever is designated as the submit button.
validate(dataObject) for “output” pages
Used on result pages / nav bar.
Loops over display-only elements.
Checks that what’s rendered matches the data object.
What’s in it for you:
You can avoid writing bespoke form-filling methods for every page.
You get a reusable, convention-based mechanism:
“If the field names line up, the form will fill itself.”
You still can wrap this in nice domain-specific methods:
signInSuccessfully(user)→ under the hood uses genericfillFormandsubmitForm.
8. Data as the Glue Between UI and API
Titus then connects the dots between:
UI tests (Selenium/page objects), and
API calls (REST Assured).
Pattern:
Use the same data object to:
Create entities via API (
UserApi.create(user)),Validate them via UI,
Or vice versa.
Example flow:
Create user via API just-in-time.
Log in through UI using that user’s credentials.
Use the same User object to:
Validate UI state (
navBar.validate(user)),Or fetch via API and compare.
Key idea:
Data objects become a shared contract across UI, API, and internal state.
This is hugely valuable when:
You’re testing complex systems where UI and API both exist.
You want to decouple state setup from the UI (faster, more reliable).
You want to assert “end-to-end correctness” without duplicating logic.
9. Big Picture: How to Apply This in Your Own Framework
As he wraps up, Titus reinforces some guiding principles:
Prefer declarative tests:
Tests say what scenario they check, not how to click each element.
Treat data with the same respect you give page objects:
Semantic data objects, not anonymous strings.
Random-by-default, override only what matters.
Avoid over-engineered data-driven and naive BDD setups:
Clarity beats cleverness every time.
Use data objects as the hub:
Page objects accept them,
APIs consume/produce them,
Validation compares against them.
These patterns are demonstrated in Java + Selenium + REST Assured, but the ideas carry over to:
C#, Python, JavaScript/TypeScript,
API-only frameworks,
And even AI-assisted “test generation” workflows where you still want humans (and machines) to reason about scenarios cleanly.
