Data Objects that Scale: How Titus Turns Messy Test Data into Clean, Maintainable Automation

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, password

  • Constructor + 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 email and password are 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 password field.

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, country only.

      • 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:

    • BrowserTextField

    • BrowserElement

  • 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 sendKeys on 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 generic fillForm and submitForm.

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:

  1. Create user via API just-in-time.

  2. Log in through UI using that user’s credentials.

  3. 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.

Leave a Comment

Your email address will not be published. Required fields are marked

{"email":"Email address invalid","url":"Website address invalid","required":"Required field missing"}