Lesson 4 of 9 · Programming for Testers
Data Structures You Will Use
Arrays, maps, sets, objects, queues and nested JSON in test automation code, with JavaScript/TypeScript examples and Java and Python equivalents, plus when each one is the right choice.
beginner 8 min read Updated
You will learn to
- Choose between arrays, objects, maps and sets for a given test-automation task
- Transform and query test data with map, filter, reduce, find and some
- Use sets and maps to detect duplicates, compare collections and count occurrences
- Recognise the equivalent structures in Java and Python
Prerequisites
- Programming Fundamentals for Testers
- JavaScript Essentials
You do not need to implement red-black trees to write good automation. You do need to pick the right built-in structure dozens of times a day: a list of rows from a table, a lookup of users by role, a set of IDs to check for duplicates, a queue of pages to crawl. Choosing well makes assertions shorter, tests faster and failure messages clearer.
This lesson covers the structures that appear constantly in test code, using JavaScript and TypeScript as the main language with Java and Python equivalents. All JavaScript examples run in Node 22 or the JavaScript Lab.
The short list
| Need | JavaScript / TypeScript | Java | Python |
|---|---|---|---|
| Ordered items, duplicates allowed | Array |
List (ArrayList) |
list |
| Fixed-shape record | object / interface |
class or record |
dataclass, dict, TypedDict |
| Key to value lookup | Map or plain object |
Map (HashMap) |
dict |
| Unique values | Set |
Set (HashSet) |
set |
| First in, first out | Array with push/shift |
ArrayDeque (as Queue) |
collections.deque |
| Last in, first out | Array with push/pop |
ArrayDeque (as Deque) |
list with append/pop |
Arrays (lists)
Arrays hold ordered values. In tests they typically contain table rows, API result items, or lists of inputs for data-driven tests.
The methods you will use most
const products = [
{ sku: 'A1', name: 'Kettle', price: 39.99, inStock: true },
{ sku: 'B2', name: 'Toaster', price: 24.5, inStock: false },
{ sku: 'C3', name: 'Blender', price: 59.0, inStock: true },
];
const names = products.map((p) => p.name); // ['Kettle', 'Toaster', 'Blender']
const available = products.filter((p) => p.inStock); // 2 items
const blender = products.find((p) => p.sku === 'C3'); // the object, or undefined
const anyOutOfStock = products.some((p) => !p.inStock); // true
const allPriced = products.every((p) => p.price > 0); // true
const total = products.reduce((sum, p) => sum + p.price, 0); // about 123.49 (floating point)
These replace most hand-written loops in assertions. Compare:
// Loop version
let allPositive = true;
for (const p of products) {
if (p.price <= 0) allPositive = false;
}
// Intent-revealing version
const allPositive2 = products.every((p) => p.price > 0);
Sorting: a classic test bug
sort() sorts in place and, without a comparator, compares values as strings:
[10, 9, 100].sort(); // [10, 100, 9]: string order
[10, 9, 100].sort((a, b) => a - b); // [9, 10, 100]
const prices = products.map((p) => p.price);
const sortedCopy = prices.toSorted((a, b) => a - b); // Node 20+: returns a new array
A common UI test checks that a list is sorted. Compare the displayed values to a sorted copy, never sort the original in place and compare it with itself:
import { test, expect } from '@playwright/test';
test('prices are sorted low to high', async ({ page }) => {
await page.goto('/products?sort=price-asc');
const texts = await page.getByTestId('price').allInnerTexts();
const prices = texts.map((t) => Number(t.replace(/[^0-9.]/g, '')));
expect(prices).toEqual(prices.toSorted((a, b) => a - b));
});
Objects and records
A plain object groups named fields describing one thing: a user, an order, a configuration. In TypeScript you describe its shape with an interface or type alias, covered in TypeScript for Testers.
interface User {
id: string;
email: string;
roles: string[];
}
const admin: User = { id: 'u1', email: 'admin@example.com', roles: ['admin'] };
Objects are also often used as simple lookups (config.baseUrl), and you can iterate them with Object.keys, Object.values and Object.entries:
const headers = { 'content-type': 'application/json', 'x-request-id': 'abc' };
for (const [name, value] of Object.entries(headers)) {
console.log(`${name}: ${value}`);
}
Maps
A Map is a dedicated key-value structure. Prefer it over a plain object when keys are dynamic, are not strings, or when you add and remove entries frequently. It also keeps insertion order and has a size property.
Counting occurrences
A very common test task: count how often something appears, for example status codes from a batch of API calls.
const statuses = [200, 200, 404, 200, 500, 404];
const counts = new Map();
for (const s of statuses) {
counts.set(s, (counts.get(s) ?? 0) + 1);
}
console.log(counts); // Map(3) { 200 => 3, 404 => 2, 500 => 1 }
Grouping
Node 21+ has Map.groupBy and Object.groupBy:
const byStock = Object.groupBy(products, (p) => (p.inStock ? 'inStock' : 'outOfStock'));
// { inStock: [Kettle, Blender], outOfStock: [Toaster] }
Lookup tables for test data
const users = new Map<string, { email: string; password: string }>([
['admin', { email: 'admin@example.com', password: process.env.ADMIN_PW ?? '' }],
['viewer', { email: 'viewer@example.com', password: process.env.VIEWER_PW ?? '' }],
]);
const creds = users.get('admin');
if (!creds) throw new Error('No test user configured for role "admin"');
get returns undefined for a missing key, so check it and throw a helpful message rather than letting undefined leak into a login form.
Sets
A Set holds unique values with fast membership checks. Tests use sets for duplicates and for comparing collections regardless of order.
const ids = ['o1', 'o2', 'o3', 'o2'];
const unique = new Set(ids);
const hasDuplicates = unique.size !== ids.length; // true
unique.has('o3'); // true
Comparing two collections ignoring order and duplicates:
const expected = new Set(['admin', 'editor', 'viewer']);
const actual = new Set(['viewer', 'admin']);
const missing = [...expected].filter((r) => !actual.has(r)); // ['editor']
const extra = [...actual].filter((r) => !expected.has(r)); // []
Reporting missing and extra gives far better failure messages than “arrays not equal”. Node 22 also has built-in set methods such as difference, intersection and union:
expected.difference(actual); // Set(1) { 'editor' }
Queues and stacks
You rarely need these explicitly, but they appear in crawlers, retry logic and tree traversal.
// Queue: breadth-first crawl of internal links, visiting each page once
const queue = ['/'];
const visited = new Set();
while (queue.length > 0) {
const path = queue.shift();
if (visited.has(path)) continue;
visited.add(path);
// const links = await getInternalLinks(path);
// queue.push(...links);
}
shift() is fine for small queues. For very large ones, an index pointer or a dedicated structure avoids re-indexing the array each time.
Nested data: JSON responses
API responses combine all of the above: objects containing arrays of objects containing maps. Destructuring and the array methods let you assert precisely:
const response = {
orders: [
{ id: 'o1', status: 'shipped', items: [{ sku: 'A1', qty: 2 }] },
{ id: 'o2', status: 'pending', items: [{ sku: 'B2', qty: 1 }, { sku: 'C3', qty: 1 }] },
],
};
const totalItems = response.orders
.flatMap((o) => o.items)
.reduce((sum, i) => sum + i.qty, 0); // 4
const pendingIds = response.orders.filter((o) => o.status === 'pending').map((o) => o.id); // ['o2']
For quick exploration of large payloads, a query language like JSONPath helps; try expressions in the JSONPath Tester.
Equality: reference vs value
Two arrays or objects with the same content are not equal with ===, because === compares references:
[1, 2] === [1, 2]; // false
new Set([1]).has([1]); // false for the same reason
Test frameworks provide deep equality: toEqual in Playwright, Jest and Vitest; assertEquals on collections in JUnit (which uses equals); == in Python, which compares lists and dicts by value. Map and Set keys are compared by reference for objects, so use strings or numbers as keys.
Java and Python equivalents
The same ideas in the other two common automation languages.
import java.util.*;
import java.util.stream.*;
List<Integer> statuses = List.of(200, 200, 404, 500);
Map<Integer, Long> counts = statuses.stream()
.collect(Collectors.groupingBy(s -> s, Collectors.counting()));
Set<String> expected = Set.of("admin", "editor");
Set<String> actual = new HashSet<>(List.of("admin"));
Set<String> missing = new HashSet<>(expected);
missing.removeAll(actual); // [editor]
from collections import Counter
statuses = [200, 200, 404, 500]
counts = Counter(statuses) # Counter({200: 2, 404: 1, 500: 1})
expected = {"admin", "editor"}
actual = {"admin"}
missing = expected - actual # {'editor'}
prices = [24.5, 39.99, 59.0]
assert prices == sorted(prices), f"not sorted: {prices}"
Note that List.of and Set.of in Java create immutable collections; calling add on them throws UnsupportedOperationException. Wrap them in new ArrayList<>(...) or new HashSet<>(...) when you need to modify them. See Java for Testers and Python for Testers.
Choosing quickly
| Question | Structure |
|---|---|
| Does order matter, or do I need duplicates? | Array / list |
| Do I look things up by key? | Map / dict |
| Do I care only whether something is present, or need uniqueness? | Set |
| Does each item have named fields? | Object, record or class |
| Do I process items in arrival order? | Queue |
Practice exercise
Given this API response of user sessions, write JavaScript that produces: (a) the number of sessions per browser, (b) a list of user IDs that appear more than once, and (c) whether every session duration is positive. Use Map, Set and array methods, not manual index loops.
const sessions = [
{ userId: 'u1', browser: 'chromium', durationSec: 120 },
{ userId: 'u2', browser: 'firefox', durationSec: 45 },
{ userId: 'u1', browser: 'webkit', durationSec: 300 },
{ userId: 'u3', browser: 'chromium', durationSec: 0 },
{ userId: 'u2', browser: 'chromium', durationSec: 60 },
];
Show a model answer
// (a) sessions per browser
const perBrowser = new Map();
for (const { browser } of sessions) {
perBrowser.set(browser, (perBrowser.get(browser) ?? 0) + 1);
}
// Map(3) { 'chromium' => 3, 'firefox' => 1, 'webkit' => 1 }
// (b) users seen more than once
const seen = new Set();
const repeated = new Set();
for (const { userId } of sessions) {
if (seen.has(userId)) repeated.add(userId);
seen.add(userId);
}
const repeatedUsers = [...repeated]; // ['u1', 'u2']
// (c) every duration positive
const allPositive = sessions.every((s) => s.durationSec > 0); // false: u3 has 0
const offenders = sessions.filter((s) => s.durationSec <= 0); // useful in the failure messageUsing a second Set for repeated ensures each user is listed once even if they appear three times. Returning the offenders alongside the boolean gives a test failure message that says exactly which records are wrong.
Key takeaways
- Arrays for ordered data, maps for lookups and counts, sets for uniqueness and order-free comparison, objects or records for fixed-shape data.
map,filter,find,some,everyandreduceexpress most test checks more clearly than loops.sort()mutates and sorts as strings by default; use a comparator andtoSortedfor comparisons.===compares references; use your framework’s deep equality for collections.- Report missing and extra items in failures instead of a bare “not equal”.
Key terms
Finished this lesson?
Progress is saved in this browser. No account needed.
Practise it
Related interview questions
- Using Java streams, find the duplicate values in a list and count their occurrences.
- What is the difference between == and ===, and how do you compare objects in tests?
- Which TypeScript utility types do you use most, and how do they help in test code?
- Compare Promise.all, Promise.allSettled, Promise.race and Promise.any. When would you use each in test code?