Writing and Calling Functions

8 min read

The first time you write the same six lines twice in two test files, you've discovered functions. A function is a named, reusable block of code — write it once, give it a name, and call it everywhere you need that logic. Functions are how test code stops being a wall of repetition and starts being a kit of parts. This lesson teaches the syntax for writing and calling functions, the difference between functions that do things and functions that return things, and the naming conventions that make a test suite readable in six months.

Why functions matter

In a test suite, the same logic appears everywhere — log in as a user, build a request URL, format a result line. Without functions you copy-paste those blocks; with functions you write them once.

Three benefits in QA work specifically:

  • DRY (don't repeat yourself). One bug fix, one place to fix it. Six copies of the login flow means six chances to forget to update one of them.
  • Readable test bodies. A test that reads loginAs(admin); openOrder(123); cancelOrder(); is shorter and clearer than 30 lines of cy.get(...).type(...).click(...) calls.
  • Reusable utilities. A generateTestEmail() helper is something every test in your suite eventually wants.

Declaring a function

The classic syntax — the one you'll meet most often — uses the function keyword:

function checkStatus(code) {
  if (code === 200) {
    console.log("✅ Status OK");
  } else {
    console.log(`❌ Status ${code}`);
  }
}

The parts:

  • function — the keyword that tells JavaScript you're defining a function.
  • checkStatus — the name. Use this to call it later.
  • (code) — the parameter list. code is a placeholder; whatever you pass in becomes its value inside the function.
  • { ... } — the body. This code runs when the function is called.

Calling a function

Once defined, a function does nothing on its own. You call it (or invoke it) by writing its name followed by parentheses, with any arguments inside.

checkStatus(200);  // ✅ Status OK
checkStatus(404);  // ❌ Status 404

The parentheses are mandatory — even when there are no arguments. checkStatus (without parentheses) is the function itself (a value you can pass around); checkStatus() is the call (the act of running it).

Functions with no parameters

Some functions don't need any input — they generate or read something on their own. The parentheses stay, just empty:

function getTimestamp() {
  return Date.now();
}
 
console.log(getTimestamp());

Output (yours will differ):

1715000000000

Date.now() is a built-in function that returns the current time as a number of milliseconds since 1970. Useful for generating unique-per-run test data.

Side effects vs return values

There are two flavours of function, and the distinction matters in test code.

Side-effect functions do something — print to the console, click a button, write a file. They typically don't return anything useful.

function logFailure(testName) {
  console.log(`[FAIL] ${testName}`);
}
 
logFailure("login");  // [FAIL] login

Return-value functions compute something and hand the result back with return. They don't usually print or click; the caller decides what to do with the answer.

function formatTestResult(name, status, durationMs) {
  return `${name}: ${status} (${durationMs}ms)`;
}
 
const line = formatTestResult("login", "PASS", 124);
console.log(line);

Output:

login: PASS (124ms)

Notice the function itself never prints — it only returns. The caller (console.log(line)) does the printing. That separation makes the function reusable: you can later log the line, write it to a file, send it over the network, or build it into a longer report — without changing the function.

A real test data helper

A common QA need: generate unique email addresses for test users so tests don't collide.

function generateEmail(prefix) {
  const stamp = Date.now();
  return `${prefix}_${stamp}@test.com`;
}
 
console.log(generateEmail("admin"));
console.log(generateEmail("member"));

Output (yours will differ):

admin_1715000000000@test.com
member_1715000000001@test.com

Each call returns a fresh string. Run the tests twice and you get two new sets of emails — no clashes, no flakes from shared state.

Function input → processing → output

That picture is every function. A response goes in, a verdict comes out. The body is whatever logic gets you from one to the other.

Naming conventions

A function name is the most important documentation in your test suite. The convention is verb + noun in camelCase:

  • validateResponse — verb (validate) + noun (response)
  • createUser — does one thing, says exactly what
  • getTestData — fetches and returns
  • formatReportLine — produces a value
  • clearCache — performs an action

Boolean-returning functions read naturally with an is, has, or should prefix:

  • isLoggedIn(user)
  • hasAdminRole(user)
  • shouldRetry(error)

Avoid vague names:

  • data() — what data?
  • process() — process what, into what?
  • doStuff() — be specific or you're just creating a black box.

A test that reads if (shouldRetry(error)) await retry() is self-explanatory; one that reads if (check(error)) f() is not.

⚠️ Common mistakes

  • Forgetting the parentheses when calling. checkStatus(200) calls the function; checkStatus 200 is a syntax error and checkStatus, 200 does nothing useful. Always include (), even for no-argument calls.
  • Mixing side effects and return values in one function. A function that both prints AND returns the same string can be hard to compose later. Pick one job per function — either it returns a value (let the caller decide what to do with it), or it performs an action.
  • Vague names. A function called validate could validate anything. validateResponse, validateLoginPayload, validateOrderTotal — the more specific the name, the more useful the function is in search results six months later.

🎯 Practice task

Build a small set of test helpers. 15-20 minutes.

  1. In your js-for-qa folder, create helpers.js.
  2. Write a function formatTestResult(name, status, durationMs) that returns a string in the form "login: PASS (124ms)".
  3. Write a function generateEmail(prefix) that returns prefix_<timestamp>@test.com.
  4. Write a function isPass(result) that returns true only when result.status === "pass" (boolean naming!).
  5. At the bottom of the file, call each function several times and console.log the results.
  6. Run with node helpers.js. Confirm each helper produces the expected output.
  7. Stretch: add a summarise(results) function that takes an array of { name, status } objects and returns a string like "3/5 passed (60%)". Use the array's length and filter (you'll formalise array methods in chapter 4 — for now, search for Array.prototype.filter and try it).

Functions are the building blocks of every test suite. The next lesson goes deeper into parameters, default values, and the return keyword.

// tip to track lessons you complete and pick up where you left off across devices.