Why Testers Need Programming Skills
Test automation means writing code. You do not need to become a software developer, but you need enough programming knowledge to write, read, and maintain test scripts. This lesson covers the essential programming concepts every QA automation engineer needs.
We use JavaScript for examples because it is the most widely used language in modern test automation (Playwright, Cypress, WebdriverIO). The concepts apply to any language.
Variables and Data Types
Variables store data that your tests use — user credentials, URLs, expected values, element selectors.
Declaring Variables
// Use const for values that won't change
const BASE_URL = 'https://app.example.com';
const TIMEOUT = 30000;
// Use let for values that will change
let loginAttempts = 0;
let currentUser = null;
Data Types
| Type | Example | Use in Testing |
|---|---|---|
| String | 'hello', "world" | URLs, selectors, expected text |
| Number | 42, 3.14 | Timeouts, counts, prices |
| Boolean | true, false | Feature flags, test conditions |
| Null | null | Empty/missing values |
| Undefined | undefined | Uninitialized variables |
| Array | [1, 2, 3] | Lists of test data |
| Object | {name: 'John'} | Structured test data |
String Operations for Testing
// Common string operations in tests
const pageTitle = 'Welcome to Dashboard - MyApp';
// Check if string contains expected text
pageTitle.includes('Dashboard'); // true
// Extract parts of strings
pageTitle.toLowerCase(); // 'welcome to dashboard - myapp'
pageTitle.trim(); // Remove whitespace
// Template literals for dynamic values
const url = `${BASE_URL}/users/${userId}/profile`;
const errorMsg = `Expected ${expected} but got ${actual}`;
Conditionals
Conditionals let your tests make decisions based on conditions.
// Basic if-else
if (userRole === 'admin') {
await page.click('#admin-panel');
} else if (userRole === 'editor') {
await page.click('#editor-tools');
} else {
await page.click('#user-dashboard');
}
// Ternary operator for simple conditions
const timeout = isCI ? 60000 : 30000;
// Checking multiple conditions
if (statusCode >= 200 && statusCode < 300) {
console.log('Request successful');
} else if (statusCode === 404) {
console.log('Resource not found');
} else {
console.log(`Unexpected status: ${statusCode}`);
}
Comparison Operators
| Operator | Meaning | Example |
|---|---|---|
=== | Strict equality | status === 200 |
!== | Not equal | role !== 'guest' |
>, < | Greater/less than | count > 0 |
>=, <= | Greater/less or equal | price >= 9.99 |
&& | AND | isVisible && isEnabled |
|| | OR | isAdmin || isSuperuser |
Loops
Loops repeat actions — essential for data-driven testing and batch operations.
For Loop
// Test login with multiple users
const users = ['admin', 'editor', 'viewer'];
for (let i = 0; i < users.length; i++) {
console.log(`Testing user: ${users[i]}`);
}
// Modern for...of loop (preferred)
for (const user of users) {
console.log(`Testing user: ${user}`);
}
forEach and map
// Process each test result
const results = [
{ test: 'login', status: 'pass' },
{ test: 'checkout', status: 'fail' },
{ test: 'search', status: 'pass' }
];
// forEach - do something with each item
results.forEach(result => {
console.log(`${result.test}: ${result.status}`);
});
// map - transform each item
const testNames = results.map(r => r.test);
// ['login', 'checkout', 'search']
// filter - keep items matching condition
const failures = results.filter(r => r.status === 'fail');
// [{ test: 'checkout', status: 'fail' }]
Functions
Functions are the building blocks of clean test code. They encapsulate reusable logic.
Basic Functions
// Function declaration
function calculateTotal(price, quantity, taxRate) {
const subtotal = price * quantity;
const tax = subtotal * taxRate;
return subtotal + tax;
}
// Arrow function (modern syntax)
const calculateTotal = (price, quantity, taxRate) => {
const subtotal = price * quantity;
const tax = subtotal * taxRate;
return subtotal + tax;
};
// Using the function
const total = calculateTotal(29.99, 3, 0.08);
Functions in Test Automation
// Reusable login function
async function login(page, username, password) {
await page.goto('/login');
await page.fill('#username', username);
await page.fill('#password', password);
await page.click('#submit');
await page.waitForURL('/dashboard');
}
// Reusable assertion helper
function assertInRange(actual, min, max) {
if (actual < min || actual > max) {
throw new Error(`${actual} is not in range [${min}, ${max}]`);
}
}
// Using these in tests
await login(page, 'admin@test.com', 'password123');
assertInRange(responseTime, 0, 3000);
Collections: Arrays and Objects
Arrays
// Test data as arrays
const validEmails = ['user@test.com', 'admin@company.org', 'test+tag@gmail.com'];
const invalidEmails = ['', 'not-an-email', '@missing.com', 'no-domain@'];
// Useful array methods
validEmails.length; // 3
validEmails.includes('user@test.com'); // true
validEmails.push('new@test.com'); // Add item
validEmails.indexOf('admin@company.org'); // 1
Objects
// Test user as an object
const testUser = {
name: 'Jane Smith',
email: 'jane@test.com',
role: 'admin',
permissions: ['read', 'write', 'delete']
};
// Accessing properties
console.log(testUser.name); // 'Jane Smith'
console.log(testUser['email']); // 'jane@test.com'
// Page object selectors as an object
const selectors = {
loginForm: '#login-form',
emailInput: '[data-testid="email"]',
passwordInput: '[data-testid="password"]',
submitButton: 'button[type="submit"]'
};
Async/Await
Test automation is heavily asynchronous — waiting for pages to load, elements to appear, API responses to return. Understanding async/await is essential.
The Problem
// Without async/await - callbacks become nested and unreadable
page.goto('/login', function() {
page.fill('#email', 'test@test.com', function() {
page.click('#submit', function() {
// This is "callback hell"
});
});
});
The Solution: Async/Await
// With async/await - clean and readable
async function testLogin(page) {
await page.goto('/login');
await page.fill('#email', 'test@test.com');
await page.fill('#password', 'secret123');
await page.click('#submit');
await page.waitForSelector('.dashboard');
}
The await keyword pauses execution until the asynchronous operation completes. The async keyword marks a function that contains await calls.
Common Async Patterns in Testing
// Wait for multiple things
const [response] = await Promise.all([
page.waitForResponse('/api/users'),
page.click('#load-users')
]);
// Wait with timeout
await page.waitForSelector('.modal', { timeout: 5000 });
// Retry pattern
async function waitForElement(page, selector, retries = 3) {
for (let i = 0; i < retries; i++) {
try {
await page.waitForSelector(selector, { timeout: 2000 });
return true;
} catch {
console.log(`Retry ${i + 1}/${retries}`);
}
}
return false;
}
Error Handling
Tests fail. Good error handling ensures failures are reported clearly and cleanup always happens.
Try-Catch
async function testCheckout(page) {
try {
await page.goto('/checkout');
await page.fill('#card', '4242424242424242');
await page.click('#pay');
await expect(page.locator('.success')).toBeVisible();
} catch (error) {
console.error(`Checkout test failed: ${error.message}`);
// Take screenshot for debugging
await page.screenshot({ path: 'checkout-failure.png' });
throw error; // Re-throw to mark test as failed
}
}
Finally Block
async function testWithCleanup(page) {
let createdUserId;
try {
// Create test data
createdUserId = await createTestUser();
// Run test
await page.goto(`/users/${createdUserId}`);
await expect(page.locator('.user-name')).toHaveText('Test User');
} finally {
// Cleanup always runs, even if test fails
if (createdUserId) {
await deleteTestUser(createdUserId);
}
}
}
Practical Patterns for Testing
Test Data Builder
function createUser(overrides = {}) {
return {
name: 'Default User',
email: `user_${Date.now()}@test.com`,
role: 'viewer',
active: true,
...overrides
};
}
// Usage
const admin = createUser({ role: 'admin', name: 'Admin User' });
const inactive = createUser({ active: false });
Configuration Management
const config = {
baseUrl: process.env.BASE_URL || 'http://localhost:3000',
apiUrl: process.env.API_URL || 'http://localhost:8080',
timeout: parseInt(process.env.TIMEOUT) || 30000,
headless: process.env.CI === 'true'
};
Exercise: Write Your First Test Helper Functions
Write the following functions in JavaScript:
generateEmail()— returns a unique email address for test dataformatPrice(amount, currency)— formats a price like “$29.99” or “EUR 29.99”retryAction(action, maxRetries)— retries an async action up to maxRetries timescompareArrays(arr1, arr2)— returns true if arrays contain the same elements
These patterns appear constantly in real test automation projects.
Key Takeaways
- Master variables, functions, conditionals, and loops — they are 80% of test code
- Learn async/await thoroughly — all modern test frameworks use it
- Use try-catch-finally for reliable error handling and cleanup
- Objects and arrays are your primary data structures for test data
- Functions should be small, focused, and reusable across tests