---
title: Test Data, Config and Environments | TestingBot
description: 'Our page objects still contain two things that do not belong there:
  the site&#39;s URL and, in the tests, the literal `&quot;student&quot;` /…'
source_url:
  html: https://testingbot.com/resources/courses/page-object-model/test-data-and-config
  md: https://testingbot.com/resources/courses/page-object-model/test-data-and-config.md
---

Course

[Page Object Model and Framework Design](https://testingbot.com/resources/courses/page-object-model)

Chapter 6 of 10

1. [1 What Is the Page Object Model?](https://testingbot.com/resources/courses/page-object-model)
2. [2 Your First Page Object in Selenium](https://testingbot.com/resources/courses/page-object-model/first-page-object)
3. [3 Page Object Model in Playwright](https://testingbot.com/resources/courses/page-object-model/playwright-page-objects)
4. [4 Components and Fluent Page Objects](https://testingbot.com/resources/courses/page-object-model/components-and-fluent-pages)
5. [5 Page Factory vs Plain By Locators](https://testingbot.com/resources/courses/page-object-model/page-factory-vs-by-locators)
6. [6 Test Data, Config and Environments](https://testingbot.com/resources/courses/page-object-model/test-data-and-config)
7. [7 Test Automation Framework Structure](https://testingbot.com/resources/courses/page-object-model/framework-layout)
8. [8 Run the Framework in Parallel on a Grid](https://testingbot.com/resources/courses/page-object-model/parallel-on-a-grid)
9. [9 POM Anti-Patterns and When to Skip POM](https://testingbot.com/resources/courses/page-object-model/anti-patterns)
10. [10 Knowledge check](https://testingbot.com/resources/courses/page-object-model/knowledge-check)

[Exit course](https://testingbot.com/resources/courses)

 Chapter 6 of 10 Intermediate 9 min read 3 quick checks Updated Nov 2026 

# Test Data, Config and Environments
On this page
1. [Three kinds of values](https://testingbot.com#three-kinds-of-values)
2. [Java: a Config class with a clear precedence](https://testingbot.com#java-a-config-class-with-a-clear-precedence)
3. [Java: test data as types](https://testingbot.com#java-test-data-as-types)
4. [Python: a settings fixture](https://testingbot.com#python-a-settings-fixture)
5. [Playwright: option fixtures](https://testingbot.com#playwright-option-fixtures)
6. [Data that survives parallel runs](https://testingbot.com#data-that-survives-parallel-runs)

Our page objects still contain two things that do not belong there: the site's URL and, in the tests, the literal `"student"` / `"Password123"`. The day you need to run the same suite against staging, or a developer's local build, or with a different account, both have to move. This chapter adds the layer that holds them.

## Three kinds of values[#](https://testingbot.com#three-kinds-of-values)

Split everything a test needs from outside itself into three buckets, because each has different rules:

| Kind | Examples | Where it lives |
| --- | --- | --- |
| **Environment config** | base URL, which browser, local vs remote grid | Per-environment file, overridable from the command line |
| **Secrets** | passwords, API keys, grid credentials | Environment variables or your CI's secret store, never in git |
| **Test data** | the user to log in as, the wrong password to try | Code (builders, data providers), derived from config |

The practice site publishes its demo password, so committing it is harmless here. For any real environment, the password goes in the second bucket, and the config file only names the account.

The suite must log in to staging as `qa-student`. Where does that account's password belong?

 In `staging.properties`, next to the username Hard-coded in the test that needs it In an environment variable or the CI secret store, with the config file naming only the account In the page object's constructor

Secrets never go in git. The config file says which account; the password arrives from the environment, which is why `users.student.password` maps to `USERS_STUDENT_PASSWORD`.

## Java: a Config class with a clear precedence[#](https://testingbot.com#java-a-config-class-with-a-clear-precedence)

Put one properties file per environment in `src/test/resources/env/`:

```properties
# src/test/resources/env/practice.properties
baseUrl=https://practicetestautomation.com
target=local
browser=chrome
users.student.username=student
users.student.password=Password123
```

```properties
# src/test/resources/env/staging.properties
baseUrl=https://staging.example.com
target=testingbot
browser=chrome
users.student.username=qa-student
# no password here: supply USERS_STUDENT_PASSWORD from the CI secret store
```

Then a `Config` class that resolves every key in a fixed order: a `-D` system property wins, then an environment variable, then the file.

```java
package com.example.framework;

import java.io.IOException;
import java.io.InputStream;
import java.io.UncheckedIOException;
import java.util.Properties;

public final class Config {
    private static final String ENV =
            System.getProperty("env", System.getenv().getOrDefault("TEST_ENV", "practice"));
    private static final Properties FILE = load(ENV);

    private Config() {}

    /** -Dkey=value, then KEY_IN_CAPS env var, then env/<ENV>.properties. */
    public static String get(String key) {
        String fromSysProp = System.getProperty(key);
        if (fromSysProp != null) return fromSysProp;

        String envName = key.replace('.', '_').toUpperCase();
        String fromEnv = System.getenv(envName);
        if (fromEnv != null) return fromEnv;

        String fromFile = FILE.getProperty(key);
        if (fromFile != null) return fromFile;

        throw new IllegalStateException(
                "Missing config '" + key + "' for env '" + ENV + "' (or env var " + envName + ")");
    }

    public static String baseUrl() { return get("baseUrl"); }
    public static String target() { return get("target"); }
    public static String browser() { return get("browser"); }

    private static Properties load(String env) {
        String path = "env/" + env + ".properties";
        Properties props = new Properties();
        try (InputStream in = Config.class.getClassLoader().getResourceAsStream(path)) {
            if (in == null) throw new IllegalStateException("No config file " + path);
            props.load(in);
        } catch (IOException e) {
            throw new UncheckedIOException(e);
        }
        return props;
    }
}
```

Two details are deliberate. A missing key throws immediately with the key, the environment and the env var name in the message; silently defaulting a password to an empty string produces a confusing login failure five minutes into a CI run. And `users.student.password` maps to `USERS_STUDENT_PASSWORD`, so CI can inject the secret without anyone editing a file.

`LoginPage.open()` now uses `driver.get(Config.baseUrl() + "/practice-test-login/")`. Switching environments is `mvn test -Denv=staging`.

## Java: test data as types[#](https://testingbot.com#java-test-data-as-types)

Tests should not pass loose strings around. A small record gives the data a name and lets you derive variations:

```java
package com.example.data;

import com.example.framework.Config;

public record User(String username, String password) {

    public static User student() {
        return new User(
                Config.get("users.student.username"),
                Config.get("users.student.password"));
    }

    public User withPassword(String newPassword) {
        return new User(username, newPassword);
    }
}
```

The negative login cases become one data-driven test:

```java
package com.example.tests;

import static org.testng.Assert.assertEquals;

import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;

import com.example.data.User;
import com.example.pages.LoginPage;

public class LoginErrorsTest extends BaseTest {

    @DataProvider(name = "badCredentials", parallel = true)
    public Object[][] badCredentials() {
        User student = User.student();
        return new Object[][] {
            { student.withPassword("wrong-password"), "Your password is invalid!" },
            { new User("not-a-user", student.password()), "Your username is invalid!" },
        };
    }

    @Test(dataProvider = "badCredentials")
    public void badCredentialsShowError(User user, String expectedError) {
        LoginPage login = new LoginPage(driver())
                .open()
                .loginExpectingError(user.username(), user.password());

        assertEquals(login.errorMessage(), expectedError);
    }
}
```

Note what did **not** change: the page object. `loginExpectingError(String, String)` takes whatever data the test hands it. Page objects that read config or pick their own test users are hard to reuse; keep data flowing in from the test.

A teammate adds `LoginPage.loginAsDefaultUser()`, which reads the username and password from `Config` itself. What is the problem?

 A page object that picks its own data is hard to reuse for other users and negative cases; data should flow in from the test Page objects are not allowed to call static methods It makes the test slower

Keep the page object about the page. The test (or a data builder) decides who logs in, so the same method serves the student, an admin and the wrong-password case.

## Python: a settings fixture[#](https://testingbot.com#python-a-settings-fixture)

The same structure in Python uses a TOML file per environment (`tomllib` is in the standard library from 3.11) and a pytest command-line option:

```toml
# config/practice.toml
base_url = "https://practicetestautomation.com"

[users.student]
username = "student"
password = "Password123"
```

```python
# settings.py
import os
import tomllib
from dataclasses import dataclass
from pathlib import Path

@dataclass(frozen=True)
class User:
    username: str
    password: str

@dataclass(frozen=True)
class Settings:
    base_url: str
    users: dict[str, User]

def load_settings(env: str) -> Settings:
    path = Path( __file__ ).parent / "config" / f"{env}.toml"
    data = tomllib.loads(path.read_text())
    users = {}
    for name, u in data.get("users", {}).items():
        # a secret in the environment beats the file: USERS_STUDENT_PASSWORD
        password = os.getenv(f"USERS_{name.upper()}_PASSWORD", u.get("password", ""))
        users[name] = User(u["username"], password)
    return Settings(base_url=os.getenv("BASE_URL", data["base_url"]), users=users)
```

```python
# conftest.py (additions)
import os

import pytest

from settings import Settings, load_settings

def pytest_addoption(parser):
    parser.addoption("--env", default=os.getenv("TEST_ENV", "practice"),
                     help="name of the file in config/ to load")

@pytest.fixture(scope="session")
def settings(request) -> Settings:
    return load_settings(request.config.getoption("--env"))

@pytest.fixture
def base_url(settings) -> str:
    return settings.base_url
```

Because `base_url` is now derived from `settings`, every test from chapter 01 picks up the environment switch without changes. Run with `pytest --env practice`. The option is named `--env` rather than `--base-url` to avoid clashing with the `pytest-base-url` plugin if someone installs it later.

Data-driven negatives use `parametrize`:

```python
import pytest

from pages.login_page import LoginPage
from settings import User

@pytest.mark.parametrize(
    "make_user, expected",
    [
        (lambda s: User(s.username, "wrong-password"), "Your password is invalid!"),
        (lambda s: User("not-a-user", s.password), "Your username is invalid!"),
    ],
    ids=["bad-password", "bad-username"],
)
def test_bad_credentials_show_error(driver, settings, make_user, expected):
    user = make_user(settings.users["student"])
    login = LoginPage(driver, settings.base_url).open()

    login.login_expecting_error(user.username, user.password)

    assert login.error_message == expected
```

The parameters are functions of the configured user rather than hard-coded strings, so the same cases work against staging with a different account.

## Playwright: option fixtures[#](https://testingbot.com#playwright-option-fixtures)

In Playwright, `baseURL` already lives in the config. Put the environment table in a module the config and the fixtures can both import:

```typescript
// config/env.ts
export type User = { username: string; password: string };

type Env = { baseURL: string; users: Record<'student', User> };

const envs: Record<string, Env> = {
  practice: {
    baseURL: 'https://practicetestautomation.com',
    users: { student: { username: 'student', password: 'Password123' } },
  },
  staging: {
    baseURL: 'https://staging.example.com',
    users: {
      student: { username: 'qa-student', password: process.env.STUDENT_PASSWORD ?? '' },
    },
  },
};

const name = process.env.TEST_ENV ?? 'practice';
const selected = envs[name];
if (!selected) throw new Error(`Unknown TEST_ENV "${name}"`);
export const env: Env = selected;
```

In `playwright.config.ts`, set `baseURL: env.baseURL`. Then expose the user as an **option fixture** , a fixture with a default value that any file or `describe` block can override with `test.use()`:

```typescript
// tests/fixtures.ts
import { test as base } from '@playwright/test';
import { LoginPage } from '../pages/login-page';
import { env, type User } from '../config/env';

type Options = { user: User };
type Pages = { loginPage: LoginPage };

export const test = base.extend<Options & Pages>({
  user: [env.users.student, { option: true }],

  loginPage: async ({ page }, use) => {
    const loginPage = new LoginPage(page);
    await loginPage.goto();
    await use(loginPage);
  },
});

export { expect } from '@playwright/test';
```

```typescript
// tests/login-errors.spec.ts
import { test, expect } from './fixtures';

const cases = [
  { name: 'bad password', change: { password: 'wrong-password' }, error: 'Your password is invalid!' },
  { name: 'bad username', change: { username: 'not-a-user' }, error: 'Your username is invalid!' },
];

for (const c of cases) {
  test(`${c.name} shows an error`, async ({ loginPage, user }) => {
    const attempt = { ...user, ...c.change };
    await loginPage.loginAs(attempt.username, attempt.password);

    await expect(loginPage.error).toHaveText(c.error);
  });
}
```

`TEST_ENV=staging npx playwright test` switches everything at once. Option fixtures can also be set per project in `playwright.config.ts`, which is how you would run the same specs as two different user roles.

You want the same Playwright specs to run once as a student and once as an admin. With the `user` option fixture, what is the cleanest way?

 Copy the spec files into an admin folder Read a `ROLE` variable inside every test Change `env.users.student` before each run Set `user` per project in `playwright.config.ts` (or with `test.use()`)

Option fixtures have a default that a project, file or `describe` block can override, so one set of specs runs under both roles without changes.

## Data that survives parallel runs[#](https://testingbot.com#data-that-survives-parallel-runs)

Read-only data, like our login, can be shared by every test. Data a test **creates or changes** cannot. Two tests that both edit "the test user's profile" at the same time will fail intermittently, and chapter 07 is about to run everything at once. Give each test its own records (a unique suffix such as a UUID in the name), create them through an API rather than the UI where you can, and clean up in the teardown of the same fixture or `@AfterMethod` that created them. Our article on [handling test flakiness](https://testingbot.com/resources/articles/handling-test-flakiness) covers shared state as a source of flaky tests in more detail, and the [Python test frameworks overview](https://testingbot.com/resources/articles/python-top-5-test-frameworks) compares how other runners handle fixtures and parametrisation.

[Previous · Chapter 5 Page Factory vs Plain By Locators](https://testingbot.com/resources/courses/page-object-model/page-factory-vs-by-locators) [Next · Chapter 7 Test Automation Framework Structure](https://testingbot.com/resources/courses/page-object-model/framework-layout)

On this page

1. [Three kinds of values](https://testingbot.com#three-kinds-of-values)
2. [Java: a Config class with a clear precedence](https://testingbot.com#java-a-config-class-with-a-clear-precedence)
3. [Java: test data as types](https://testingbot.com#java-test-data-as-types)
4. [Python: a settings fixture](https://testingbot.com#python-a-settings-fixture)
5. [Playwright: option fixtures](https://testingbot.com#playwright-option-fixtures)
6. [Data that survives parallel runs](https://testingbot.com#data-that-survives-parallel-runs)

Certification

This course has a free, hands-on certificate.

[How it works](https://testingbot.com/resources/courses/page-object-model/certification)
