Two pieces of modern JavaScript syntax change how you handle objects and arrays in test code. Destructuring unpacks values out of structures into named variables in one line. Spread does the opposite — it expands a structure into another, perfect for merging configs or copying arrays. Both are everywhere in modern test frameworks; you'll read them on day one and write them on day three.
Object destructuring
The basic shape: name the properties you want on the left, give the object on the right.
const user = { name: "Alice", role: "admin", age: 32 };
const { name, role } = user;
console.log(name); // "Alice"
console.log(role); // "admin"The names inside {} must match the property names in the object. The result is two new variables, name and role, holding the values from the user object. You can pick as many or as few properties as you want — anything you don't name is just ignored.
Renaming during destructuring
If the property name doesn't suit you (clashes with an existing variable, or is too generic), rename it on the way out with ::
const user = { name: "Alice", role: "admin" };
const { name: userName, role: userRole } = user;
console.log(userName); // "Alice"
console.log(userRole); // "admin"The syntax reads as "take property name, call it userName." Useful when destructuring multiple objects with overlapping field names — const { id: userId } = user; const { id: orderId } = order; keeps both.
Default values
If a property is missing or undefined, you can supply a fallback with =:
const config = { baseUrl: "https://staging.com" };
const { baseUrl, timeout = 5000, retries = 3 } = config;
console.log(baseUrl); // "https://staging.com"
console.log(timeout); // 5000 — used the default
console.log(retries); // 3 — used the defaultThis pairs well with default function parameters — many test helpers destructure their options object with defaults baked in: function login({ user, password = "Test@1234" }).
Array destructuring
Same idea, but with positions instead of names. Use [] and the variable name position matches the array index.
const browsers = ["Chrome", "Firefox", "Safari"];
const [first, second] = browsers;
console.log(first); // "Chrome"
console.log(second); // "Firefox"You can skip items by leaving an empty slot — useful when you only want some indices:
const browsers = ["Chrome", "Firefox", "Safari", "Edge"];
const [, , third] = browsers;
console.log(third); // "Safari"The two leading commas skip indices 0 and 1.
The rest pattern
Both forms support the rest pattern — ...name collects "everything else" into an array (for arrays) or object (for objects).
const browsers = ["Chrome", "Firefox", "Safari", "Edge"];
const [head, ...tail] = browsers;
console.log(head); // "Chrome"
console.log(tail); // ["Firefox", "Safari", "Edge"]
const user = { name: "Alice", role: "admin", email: "alice@test.com" };
const { name, ...rest } = user;
console.log(name); // "Alice"
console.log(rest); // { role: "admin", email: "alice@test.com" }Common in test code: "extract the fields I care about, keep everything else for logging."
The spread operator (...)
The same ... spelling, used in a different position, does the opposite — it expands an array or object back out, into another array or object.
Spreading arrays:
const desktop = ["Chrome", "Firefox"];
const mobile = ["Safari iOS", "Chrome Android"];
const allBrowsers = [...desktop, ...mobile];
console.log(allBrowsers);
// ["Chrome", "Firefox", "Safari iOS", "Chrome Android"][...desktop, ...mobile] builds a new array from the contents of both. The originals are untouched — spread is a non-mutating way to combine, copy, or insert into arrays.
Spreading objects:
const defaults = { timeout: 5000, retries: 3 };
const overrides = { timeout: 10000 };
const final = { ...defaults, ...overrides };
console.log(final); // { timeout: 10000, retries: 3 }Two important rules:
- The result is a new object — neither input is mutated.
- Later wins — when keys collide, the spread that comes later overrides the earlier one. That's what makes "defaults plus overrides" work: write
defaultsfirst, overrides second.
Why this matters for QA
Two patterns dominate.
Pattern 1 — extracting fields from an API response.
const response = {
status: 200,
data: {
users: [
{ id: 1, name: "Alice" },
{ id: 2, name: "Bob" }
]
}
};
const { status, data: { users } } = response;
console.log(status); // 200
console.log(users); // [{ id: 1, name: "Alice" }, { id: 2, name: "Bob" }]The pattern data: { users } is nested destructuring — reach into data, pull out users. One line replaces three.
Pattern 2 — building a config from defaults and overrides.
const defaultConfig = {
baseUrl: "https://staging.com",
timeout: 5000,
retries: 3,
headless: true
};
const stagingOverrides = {
baseUrl: "https://staging-eu.com",
timeout: 10000
};
const finalConfig = { ...defaultConfig, ...stagingOverrides };
console.log(finalConfig);Output:
{
baseUrl: 'https://staging-eu.com',
timeout: 10000,
retries: 3,
headless: true
}
baseUrl and timeout were overridden; retries and headless were inherited. Every test framework worth using has this pattern in its core — a base config plus per-environment or per-run overrides.
How a config merge actually works
Combining destructure and spread
The two are most useful together. A common helper: take an options object, destructure the fields you need with defaults, spread the rest into a child call.
function loginAs({ user, password = "Test@1234", ...extra }) {
console.log(`Logging in as ${user} with password ${password}`);
console.log("Extra options forwarded:", extra);
}
loginAs({ user: "alice", remember: true, twoFactor: false });Output:
Logging in as alice with password Test@1234
Extra options forwarded: { remember: true, twoFactor: false }
user and password get pulled out by name. Anything else gets collected into extra, which the function can forward to a lower-level helper.
⚠️ Common mistakes
- Renaming syntax mix-up.
const { name = userName }sets a default, it does NOT rename. Renaming isconst { name: userName }(colon, no equals). Both at once:const { name: userName = "anonymous" }. - Spread does a shallow copy.
const copy = { ...config }copies the top-level fields. Nested objects are still shared by reference — mutatingcopy.api.baseUrlmutatesconfig.api.baseUrltoo. For deep copies, usestructuredClone(obj)or a JSON round-trip (next lesson). - Spread order in object merges.
{ ...overrides, ...defaults }makes defaults win, which is the opposite of what you usually want. The mantra is "defaults first, overrides last."
🎯 Practice task
Build a config merge helper. 15-20 minutes.
-
In your
js-for-qafolder, createmerge.js. -
Declare
defaultConfigwithbaseUrl,timeout,retries, andheadless. -
Declare
stagingOverrideswith two of those keys (different values), andprodOverrideswith one. -
Build
stagingFinalandprodFinalusing object spread. Print both. -
From
stagingFinal, destructure{ baseUrl, timeout }into local variables. Print them. -
Declare
const response = { status: 200, data: { user: { id: 1, name: "Alice" } } };. Use one nested destructure to pullstatusand the innernameout:const { status, data: { user: { name } } } = response;Print both.
-
Stretch: write a
mergeConfig(...sources)function that uses rest parameters and returnsObject.assign({}, ...sources)(or repeated spread) to merge any number of config objects, with later ones winning. Test it with 3-4 config objects.
The next (and last) lesson of this chapter covers JSON — the format every API and fixture file in your test suite is going to be in.