Let’s be honest: almost every QA automation engineer starts their journey with a heavy reliance on for loops.
Need to find something in a JSON response? Throw a for loop at it.
Need to extract specific values? Another for loop.
There’s nothing inherently broken about a for loop. But using it everywhere leads to verbose, messy, and hard-to-maintain test code.
Let me show you a real-world API testing example and why moving to .filter() and .map() will instantly level up your framework.
Test Scenario
Imagine you receive an array of users from a /users endpoint:
[
{
"id": 101,
"personalInfo": {
"firstName": "John",
"lastName": "Smith",
"email": "john.smith@example.com"
},
"account": {
"role": "ADMIN"
}
},
{
"id": 102,
"personalInfo": {
"firstName": "Peter",
"lastName": "Jones",
"email": "peter.jones@example.com"
},
"account": {
"role": "USER"
}
},
{
"id": 103,
"personalInfo": {
"firstName": "Michael",
"lastName": "Brown",
"email": "m.brown@test.com"
},
"account": {
"role": "ADMIN"
}
}
]
The task: Extract the email addresses of all system administrators to verify notification dispatches.
The Traditional Approach: Imperative for Loop
function getAdminEmails(users) {
let adminEmails = [];
for (let i = 0; i < users.length; i++) {
if (users[i].account && users[i].account.role === 'ADMIN') {
if (users[i].personalInfo && users[i].personalInfo.email) { adminEmails.push(users[i].personalInfo.email);
}
}
}
return adminEmails;
}
It “works”, right? But here is why it creates technical debt in the long run:
High Cognitive Load: You have to manually track loop mechanics (i, users.length, i++) and mentally resolve nested properties just to understand what the code does.
State Mutation: Creating let adminEmails = [] and mutating it with .push() introduces unwanted side effects and risks leaking state between tests.
Pyramid of Doom: Notice how the code shifts horizontally to the right? Deep if nesting makes stack traces annoying to debug when tests fail.
Boundary Fragility: A single typo like using <= instead of < will cause your test runner to crash with Cannot read property of undefined.
The Clean Approach: Declarative Pipeline
Instead of telling JavaScript how to iterate step-by-step, tell it what you want to achieve.
Quick Refresher: What Do .filter() and .map() actually do?
Before we refactor our function, let’s break down the two core building blocks of declarative data processing:
.filter() – The Bouncer: It loops through an array and keeps ONLY the items that meet a specific condition (returning true). It produces a brand-new, filtered array without touching or mutating the original payload
.map() – The Transformer: It takes every item in an array, applies a transformation to it, and spits out a new array of the exact same length containing only the transformed values (e.g., turning a list of complex user objects into a simple list of email strings).
By chaining them together, you build a clean, readable pipeline that first weeds out what you don’t need, and then extracts only the exact fields your test assertions care about.
function getAdminEmails(users = []) {
return users
.filter(user => user?.account?.role === 'ADMIN')
.map(user => user?.personalInfo?.email)
.filter(Boolean);
}
Why this approach wins:
Reads like plain English: “Filter admins – > map to emails -> drop empty values” Done.
Immutability: .filter() and .map() return brand new arrays. Your original API payload stays untouched.
Null-Safety (?.): Optional chaining prevents your tests from exploding if a key is missing in the JSON response.
Summary
Moving from imperative for loops to a functional .filter() and .map() pipeline is one of the quickest ways to clean up your API test suite. It eliminates state mutation, removes deep nesting, and keeps your code safe from unexpected runtime errors.
How do you process large JSON collections in your test framework? Do you stick to .map() / .filter(), or do you have other clean code patterns you rely on?
Let me know in the comments below!