Skip to content
SDETRole
SDETRole

Lesson 1 of 9 · Programming for Testers

Programming Fundamentals for Testers

Variables, types, conditions, loops, functions and collections, taught through test-automation examples in JavaScript with Java and Python equivalents.

beginner 8 min read Updated

You will learn to

  • Use variables, data types and operators correctly in test code
  • Write conditions and loops to drive data-driven checks
  • Break repeated test logic into small, named functions
  • Store and iterate test data with arrays and objects (lists and maps)

Prerequisites

  • None. Basic computer literacy and a text editor

Automation is programming. Every Playwright test, Selenium framework and API check is code, and the quality of that code decides whether your suite is a safety net or a source of flaky noise. You do not need a computer science degree, but you do need a solid, practical grip on the fundamentals that appear in every test you will write.

This lesson uses JavaScript for its main examples because it runs in any browser and powers Playwright and Cypress. Where it helps, you will see the same idea in Java and Python, the other two languages common in test automation. The concepts transfer directly; only the syntax changes.

Why testers need programming

  • Automation: UI, API and performance tests are all written in code.
  • Reading the product: being able to read the code under test helps you find edge cases and understand defects.
  • Tooling: small scripts to generate test data, parse logs or compare files save hours.
  • Collaboration: reviewing developers’ unit tests and pull requests is far easier when you can read their code.

Running code

To follow along, use the JavaScript Lab, which runs code in your browser, or install Node.js 22 and run a file from the terminal:

node --version      # should print v22.x
node hello.js       # runs the file
// hello.js
console.log('Hello, tester');

console.log prints to the console. You will use it constantly to inspect values while you learn.

Variables and data types

A variable is a named place to store a value. In modern JavaScript, declare variables with const (cannot be reassigned) or let (can be reassigned). Avoid the older var.

const baseUrl = 'https://staging.example.com';
let retries = 0;
retries = retries + 1; // fine, retries is let

// baseUrl = 'https://prod.example.com'; // TypeError: Assignment to constant variable.

Default to const. Use let only when the value genuinely needs to change, such as a counter.

Core types

Type JavaScript example Java equivalent Python equivalent
String 'admin@example.com' String str
Number 42, 19.99 int, double int, float
Boolean true, false boolean True, False
Nothing null, undefined null None
List ['chrome', 'firefox'] List<String> list
Key-value { name: 'Ana', age: 30 } Map<String, Object> or a class dict

JavaScript is dynamically typed: a variable can hold any type. Java is statically typed: you declare the type and the compiler enforces it. TypeScript adds static types to JavaScript, which is why it is the default for Playwright projects. You will meet it in TypeScript for Testers.

// Java 21
String baseUrl = "https://staging.example.com";
int retries = 0;
var browsers = List.of("chrome", "firefox"); // var infers List<String>
# Python
base_url = "https://staging.example.com"
retries = 0
browsers = ["chrome", "firefox"]

Strings you will build all day

Test code constantly builds URLs, messages and selectors. In JavaScript, template literals (backticks) let you embed values:

const userId = 1234;
const url = `${baseUrl}/api/users/${userId}`;
console.log(url); // https://staging.example.com/api/users/1234

Operators and comparisons

Arithmetic (+ - * / %) works as you would expect. For tests, comparison operators matter more, because every assertion is a comparison.

const expected = 200;
const actual = 200;

actual === expected; // true: strict equality, same type and value
'200' === 200;       // false: string vs number
'200' == 200;        // true: loose equality converts types first. Avoid it.
actual !== 404;      // true

Logical operators combine conditions: && (and), || (or), ! (not). Java uses the same symbols; Python uses the words and, or, not.

Conditions

if / else runs different code depending on a condition. In test code you use it in helpers and setup, for example choosing a URL per environment:

const env = 'staging';
let baseUrl;

if (env === 'prod') {
  baseUrl = 'https://example.com';
} else if (env === 'staging') {
  baseUrl = 'https://staging.example.com';
} else {
  baseUrl = 'http://localhost:3000';
}

A cleaner pattern for lookups like this is an object used as a map:

const urls = {
  prod: 'https://example.com',
  staging: 'https://staging.example.com',
  local: 'http://localhost:3000',
};
const baseUrl = urls[env] ?? urls.local; // ?? falls back when the value is null or undefined

Loops

Loops repeat work. Their most important use in testing is data-driven testing: running the same check against many inputs.

const invalidEmails = ['', 'plainaddress', '@no-user.com', 'user@', 'user@@example.com'];

for (const email of invalidEmails) {
  const valid = isValidEmail(email);
  console.log(`${JSON.stringify(email)} -> ${valid}`);
}

function isValidEmail(value) {
  return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value);
}

for...of iterates over the values of an array and is the loop you will use most. The classic counting loop is still useful when you need the index or a fixed number of repetitions:

for (let attempt = 1; attempt <= 5; attempt++) {
  console.log(`Failed login attempt ${attempt}`);
}

The same data-driven idea in Java and Python:

for (String email : List.of("", "plainaddress", "user@")) {
    System.out.println(email + " -> " + isValidEmail(email));
}
for email in ["", "plainaddress", "user@"]:
    print(email, "->", is_valid_email(email))

Functions

A function is a named, reusable block of code that takes inputs (parameters) and returns an output. Functions are how you avoid copy-pasting the same ten lines into fifty tests.

function formatPrice(amount, currency = 'USD') {
  return new Intl.NumberFormat('en-US', { style: 'currency', currency }).format(amount);
}

formatPrice(19.5);         // '$19.50'
formatPrice(19.5, 'EUR');  // '€19.50'

Arrow functions are a shorter syntax you will see everywhere in modern test code:

const add = (a, b) => a + b;
const isEven = (n) => n % 2 === 0;

Good test helper functions:

  • Do one thing and are named for it: createUser, loginAs, buildOrderPayload.
  • Take inputs as parameters instead of reading global variables.
  • Return values rather than printing them, so the caller can assert on the result.

Collections: arrays and objects

Real test data is structured: a list of users, an order with line items, an API response. In JavaScript you model that with arrays (ordered lists) and objects (named fields).

const users = [
  { name: 'Ana', role: 'admin', active: true },
  { name: 'Ben', role: 'viewer', active: false },
  { name: 'Chen', role: 'editor', active: true },
];

users.length;          // 3
users[0].name;         // 'Ana'
users.at(-1).role;     // 'editor' (last item)

Array methods let you answer questions about data without writing manual loops. These come up in almost every API test:

const activeNames = users.filter((u) => u.active).map((u) => u.name);
// ['Ana', 'Chen']

const admin = users.find((u) => u.role === 'admin');
// { name: 'Ana', role: 'admin', active: true }

const allHaveRoles = users.every((u) => typeof u.role === 'string');
// true

const anyInactive = users.some((u) => !u.active);
// true

Putting it together: a tiny test runner

Here is a small, complete program combining everything above. It checks a function against a table of cases and reports results, which is the core of what every test framework does.

function calculateDiscount(total) {
  if (total >= 100) return total * 0.1;
  return 0;
}

const cases = [
  { input: 99.99, expected: 0 },
  { input: 100, expected: 10 },
  { input: 150, expected: 15 },
  { input: 0, expected: 0 },
];

let failures = 0;
for (const { input, expected } of cases) {
  const actual = calculateDiscount(input);
  const passed = actual === expected;
  if (!passed) failures++;
  console.log(`${passed ? 'PASS' : 'FAIL'} total=${input} expected=${expected} actual=${actual}`);
}
console.log(`${cases.length - failures}/${cases.length} passed`);

Notice the test cases sit on both sides of the boundary at 100. That is boundary value analysis, a design technique you will use for the rest of your career.

Practice exercise

Open the JavaScript Lab (or a local Node file) and write a function validatePassword(password) that returns an array of error messages (empty if valid) for these rules:

  • At least 12 characters
  • At least one digit
  • At least one uppercase letter

Then write a data-driven loop that runs it against at least five passwords, including one that breaks every rule and one exactly 12 characters long, and prints each result.

A good solution returns all applicable errors (not just the first), uses const by default, uses ===, and includes boundary inputs of 11 and 12 characters.

Show a model answer
function validatePassword(password) {
  const errors = [];
  if (password.length < 12) errors.push('Must be at least 12 characters');
  if (!/\d/.test(password)) errors.push('Must contain a digit');
  if (!/[A-Z]/.test(password)) errors.push('Must contain an uppercase letter');
  return errors;
}

const samples = [
  'short',            // breaks all three rules
  'Abcdefghij1',      // 11 chars: too short by one
  'Abcdefghijk1',     // 12 chars: valid boundary
  'abcdefghijk1',     // no uppercase
  'Abcdefghijkl',     // no digit
];

for (const pw of samples) {
  const errors = validatePassword(pw);
  console.log(pw, errors.length === 0 ? 'VALID' : errors.join('; '));
}

Why it is good: each rule is checked independently so the user sees every problem at once, the 11/12 boundary is covered, and the function returns data instead of printing, so it can be asserted on later.

Key takeaways

  • Test automation is programming; the fundamentals are the same in JavaScript, Java and Python.
  • Prefer const, use let only for values that change, and always compare with === in JavaScript.
  • Loops plus a table of inputs give you data-driven tests with very little code.
  • Small, well-named functions that take parameters and return values keep test code readable and reusable.
  • Arrays and objects (lists and maps) model real test data; filter, map, find, some and every answer most questions about it.

Key terms

Finished this lesson?

Progress is saved in this browser. No account needed.

Type to search across SDETRole.

Full-text search by Pagefind · runs in your browser Open search page →