Objects — Structuring Test Data

8 min read

A test user has a name, an email, a role, a flag for whether the account is verified, and a timestamp for when it was last seen. That cluster of related fields is one object — a collection of named properties, each pointing at a value. Objects are how JavaScript groups related data, and they're the shape you'll see most often in test fixtures, API responses, and configuration files. This lesson covers the syntax, the access patterns, and the small tricks that make objects work for you.

Creating an object

The literal syntax — curly braces with key: value pairs:

const user = {
  name: "Alice",
  role: "admin",
  age: 32,
  active: true
};
 
console.log(user);

Output:

{ name: 'Alice', role: 'admin', age: 32, active: true }

Each property is a key (a string-like name) paired with a value (any type — string, number, boolean, array, or another object). The order of properties doesn't matter — user.name works regardless.

Accessing properties — dot vs bracket notation

Two ways to read a property. Use dot notation by default; bracket notation when the key is computed at runtime or contains special characters.

const user = { name: "Alice", role: "admin" };
 
// Dot notation — preferred
console.log(user.name);     // "Alice"
console.log(user.role);     // "admin"
 
// Bracket notation — needed for dynamic keys
const field = "name";
console.log(user[field]);   // "Alice" — looks up user["name"]
 
// Bracket notation — needed for keys with special characters
const config = { "max-retries": 3, "default-timeout": 5000 };
console.log(config["max-retries"]);   // 3
// config.max-retries  // ❌ syntax error — JS reads it as config.max minus retries

In test code, dot notation handles 95% of cases. Bracket notation comes up most often when iterating with a variable holding the field name.

Adding, updating, and deleting

Objects are mutable by default. Assigning to a new key adds it; assigning to an existing key updates it; the delete keyword removes it.

const user = { name: "Alice" };
 
user.email = "alice@test.com";    // add
user.name = "Alicia";             // update
delete user.email;                // remove
 
console.log(user);   // { name: 'Alicia' }

Important: const prevents reassigning user to a different object, but it does NOT prevent mutating the object's contents. That's the same gotcha from the scope lesson — const is about the binding, not about the value.

Nested objects

A property can hold another object. That's how real test configurations are structured.

const config = {
  api: {
    baseUrl: "https://api.staging.com",
    timeout: 5000
  },
  browser: {
    name: "chrome",
    headless: true
  },
  auth: {
    apiKey: "test-1234",
    refreshAfterMs: 600000
  }
};
 
console.log(config.api.baseUrl);       // "https://api.staging.com"
console.log(config.browser.headless);  // true

Each . walks one level deeper. If a level might be missing, optional chaining (?.) returns undefined instead of throwing:

console.log(config.payments?.currency);  // undefined — no payments key
// console.log(config.payments.currency);  // ❌ TypeError

Optional chaining is invaluable when reading API responses where some fields are optional.

Methods — functions as values

Properties can hold functions. When they do, they're called methods.

const helper = {
  generateId: () => `id-${Date.now()}`,
  greet: function(name) {
    return `Hello, ${name}`;
  }
};
 
console.log(helper.generateId());     // "id-1715000000000"
console.log(helper.greet("Alice"));   // "Hello, Alice"

Both arrow and classic syntax work as values. (As you saw in the arrow-functions lesson, arrows behave differently with this — for object methods that need this to refer to the surrounding object, prefer classic functions.)

Iterating over an object

Three built-in helpers walk an object's properties. Each returns an array, so you can combine them with the array methods from the previous lesson.

const config = {
  baseUrl: "https://staging.com",
  timeout: 5000,
  retries: 3
};
 
console.log(Object.keys(config));    // ["baseUrl", "timeout", "retries"]
console.log(Object.values(config));  // ["https://staging.com", 5000, 3]
console.log(Object.entries(config)); // [["baseUrl","https://staging.com"], ["timeout",5000], ["retries",3]]
 
Object.entries(config).forEach(([key, value]) => {
  console.log(`${key} = ${value}`);
});

Output:

baseUrl = https://staging.com
timeout = 5000
retries = 3

Object.entries is the workhorse — [key, value] pairs let you iterate, transform, or assert on every property in turn.

Checking whether a property exists

Two ways to ask "does this object have a property called X?":

const user = { name: "Alice", email: undefined };
 
console.log("name" in user);              // true
console.log("email" in user);             // true  — exists, even though value is undefined
console.log("phone" in user);             // false
 
console.log(user.hasOwnProperty("name")); // true
console.log(user.hasOwnProperty("phone"));// false

in checks if the key exists at all, including inherited keys. hasOwnProperty is stricter — only own keys, not inherited ones. In practice, in is fine for test code 99% of the time.

A common shortcut is just to check truthiness — if (user.name) is fine when undefined and a truthy value are the only possibilities. It fails on intentionally falsy values like 0 or "", so use in or hasOwnProperty when the value could legitimately be falsy.

A real test config at a glance

config
  • – name: 'staging'
  • – baseUrl: 'https://...'
  • – apiKey
  • – testUser email
  • – defaultPassword
  • – request: 5000
  • – navigation: 30000
  • newCheckout: true –
  • darkMode: false –

Real test configs grow into trees like this — environment metadata, credentials, timeouts, feature flags. The shape might change project-to-project but the structure is always the same: a top-level object with grouped properties.

⚠️ Common mistakes

  • Treating dot notation as universal. obj["max-retries"] works; obj.max-retries is parsed as subtraction. If a key has a dash, space, or starts with a digit, you must use bracket notation.
  • Crashing on missing nested fields. response.user.profile.name throws if any segment is missing. Use optional chaining (?.) anywhere a level might be absent — response?.user?.profile?.name.
  • Confusing const with immutability. const config = {...} lets you do config.timeout = 1000. The binding is constant; the contents aren't. Be deliberate about mutation in shared objects.

🎯 Practice task

Build a test environment config. 20 minutes.

  1. In your js-for-qa folder, create env-config.js.

  2. Create a const config = {...} with at least three nested groups: environment, credentials, timeouts. Populate each with 2-3 real-looking values.

  3. Print one nested value with dot notation: console.log(config.environment.baseUrl).

  4. Print one value with bracket notation using a variable: const key = "baseUrl"; console.log(config.environment[key]).

  5. Walk the timeouts with Object.entries:

    Object.entries(config.timeouts).forEach(([k, v]) => console.log(`${k}: ${v}ms`));
  6. Add a featureFlags group at the top level, with two boolean flags. Use "darkMode" in config.featureFlags to check existence.

  7. Stretch: read config.payments?.currency (which doesn't exist). Confirm it logs undefined rather than throwing. Then add a payments group and rerun — confirm it now prints the value.

The next lesson teaches the syntax — destructuring and spread — that pulls these objects apart and merges them back together.

// tip to track lessons you complete and pick up where you left off across devices.