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 retriesIn 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); // trueEach . 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); // ❌ TypeErrorOptional 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"));// falsein 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
- – 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-retriesis 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.namethrows if any segment is missing. Use optional chaining (?.) anywhere a level might be absent —response?.user?.profile?.name. - Confusing
constwith immutability.const config = {...}lets you doconfig.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.
-
In your
js-for-qafolder, createenv-config.js. -
Create a
const config = {...}with at least three nested groups:environment,credentials,timeouts. Populate each with 2-3 real-looking values. -
Print one nested value with dot notation:
console.log(config.environment.baseUrl). -
Print one value with bracket notation using a variable:
const key = "baseUrl"; console.log(config.environment[key]). -
Walk the timeouts with
Object.entries:Object.entries(config.timeouts).forEach(([k, v]) => console.log(`${k}: ${v}ms`)); -
Add a
featureFlagsgroup at the top level, with two boolean flags. Use"darkMode" in config.featureFlagsto check existence. -
Stretch: read
config.payments?.currency(which doesn't exist). Confirm it logsundefinedrather than throwing. Then add apaymentsgroup 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.