Reading Code for Testers
Reading code, for a tester, means following what a small piece of code does with one input: which values it stores, which decisions it takes, which lines run and what comes back. You do not need to write production code for this. A few building blocks cover most business logic: variables, comparisons, the operators &&, || and !, if and switch, loops, return and throw.
Example: if (total > 200) { fee = 0; } reads "if the total is greater than 200, the fee becomes 0". If the requirement says "orders of 200 or more ship free", code and requirement disagree at exactly one value, 200. A tester who can read that line can design the test that exposes it, or ask the question before any test is run.
Why it matters
Black-box techniques such as equivalence partitioning and boundary value analysis work from the requirement. They cannot see what the code actually compares, in which order it decides or where it stops early. Reading the code shows the decisions behind the partitions and boundaries you test, and exposes behaviour no requirement mentions: an extra discount, a forgotten case, an error that ends the function.
It also changes how you work with developers. You can review a change for the rules it touches, read a unit test and see what it really checks, and name the input and the line you suspect in a bug report. The white-box lessons that follow rely on it: they explain statement, branch and condition coverage with code written exactly like the snippets here and numbered the same way.
Values, variables and operators
A variable is a name for a value. const creates a name that cannot be given a new value; let creates one that can. Values have types: numbers (200), text strings ('SAVE10' or "SAVE10"), booleans (true and false) and two kinds of "nothing", undefined (never set) and null (empty on purpose). Text after // is a comment, a note for people that never runs. Read each snippet, then say it in plain words:
const rate = 0.15;reads "rate is 0.15, and stays 0.15".let fee = 15;followed byfee = fee + 10;reads "fee starts at 15; take the current fee, add 10, store 25". A single=is assignment: it changes a value and never compares.const item = { price: 30, qty: 2 };reads "item groups two named values, price 30 and qty 2", anditem.pricereads "the price of item".[30, 20]is a list (an array) of two values,[]is an empty list, anditems.lengthis the number of values initems.constfixes only the name: the values inside a list or object can still change. In a calculation*multiplies and/divides:item.price * item.qtyreads "the price of item times its qty".total === 200reads "is total exactly 200?" and givestrueorfalse;!==reads "is not". The double==also compares but converts types first, so'200' == 200istruewhile'200' === 200isfalse.total >= 200reads "is total 200 or more?";total > 200leaves 200 itself out. Boundary defects often live in that one character.member && total >= 100reads "a member and at least 100":trueonly when both sides are.express || urgentistruewhen at least one side is.!expressreads "not express".&&stops at the first false side and||at the first true one, so the second side may never be evaluated.a || b && creadsa || (b && c), because&&is evaluated before||. Brackets change the meaning, so read them carefully.if (qty)uses a value as a condition:0, an empty string,null,undefinedandNaN("not a number", the result of a calculation such as0 / 0or5 - undefined) count asfalse. A quantity of 0 is then treated like a missing quantity. The same rule hides inqty || 1, read "qty, or 1 when qty counts as false":||and&&give back one of the two values themselves, not onlytrueorfalse, so a quantity of 0 silently becomes 1.
Decisions: if, else, switch, return and throw
A decision chooses which lines run next. The lines between { and } form a block: the decision either enters it or skips all of it. Indentation only helps the eye, while JavaScript follows the braces: an if without braces controls only the one statement after it, however the next lines are indented. So when code arrives badly indented or minified into one long line, re-indent it before you trace it. In an if / else if / else chain the conditions are checked from the top; the first one that is true runs its block and the rest of the chain is skipped, even when a later condition would also be true. The order of the conditions is therefore part of the logic.
if (total <= 0) { … } else if (total < 200) { … } else { … }reads "zero or less; otherwise under 200; otherwise everything else". Exactly one block runs.switch (coupon) { case 'SAVE10': …; break; case 'HALF': …; break; default: … }reads "compare coupon with each case using===, run the first that matches, ordefaultwhen none does". Withoutbreak, execution falls through into the next case; when nobody intended that, it is a classic defect.return fee;reads "stop here and hand fee back to the caller". Nothing after it in the same function runs, so an earlyreturnnear the top skips everything below it.throw new Error('Order total must be positive');reads "stop here with an error". The error travels to the code that called the function, which can catch it withtry/catch; if nothing catches it, the operation stops, and in a browser the console reports an uncaught error. For a tester athrowis an expected result to check: the message, and what the user or the API client receives.
Try it: JavaScript Formatter Try it: Decision Table Generator
Loops, parameters and return values
A function is named logic with inputs and an output. Its parameters are the names in brackets, the arguments of a call are the values passed in, and the return value is what comes back. That maps directly onto testing: the arguments are your test inputs and the return value is the actual result you compare with the expected result. The coverage lessons also show TypeScript, which adds types after the names: total: number reads "total is a number", items: Item[] reads "items is a list of Item values" (Item is a type the team defined elsewhere), and ): number after the brackets reads "returns a number". TypeScript checks them before the code runs and removes them from the JavaScript that actually runs, so they do not check the values a call receives at run time, for example from an API response.
A loop repeats a block. A for loop has a start, a condition checked before every round and a step after every round; a while loop repeats as long as its condition is true. Both can run zero times.
function deliveryFee(total, express)reads "a function named deliveryFee with two inputs". The calldeliveryFee(200, false)setstotalto 200 andexpresstofalse; a missing argument isundefined. A call inside the code, such asMath.floor(total / 10), runs another function and uses its return value; look built-in ones up on MDN (Math.floorrounds down:Math.floor(12.5)is 12).for (let i = 0; i < items.length; i++) { … }reads "startiat 0; whileiis less than the number of items, run the block, then add 1 toi". Positions in a list start at 0:items[0]is the first item,items[i]the item at positionianditems[items.length - 1]the last. Starting at 1 skips the first item;<=runs one round too many.for (const item of items) { … }reads "for each item in items, in order, run the block". It has no counter to get wrong, but an empty list still means zero rounds.while (attempts < 3) { … }reads "repeat while attempts is below 3". If nothing in the block changesattempts, the loop never ends and the page or the test hangs.- Trace every loop with zero, one and several rounds: an empty cart, one item, many items.
Worked example: tracing a fee function by hand
Requirement: "Delivery costs 15.00 SAR. Orders of 200.00 SAR or more get free delivery. Express delivery adds 10.00 SAR, also to orders with free delivery. A total of zero or less is rejected." The developer's function is printed below the trace steps, with line numbers as in the coverage lessons; look up each line in it as you read the steps.
Make a trace table with one row per input and columns for the lines run, the outcome of each decision and the value returned. Walk the code one line at a time, writing down each variable's new value, and only then compare the return value with the expected result taken from the requirement. Pick inputs on both sides of every comparison:
deliveryFee(0, false): on line 2,0 <= 0istrue, so line 3 throws and lines 5 to 12 never run. The requirement expects a rejection: it matches.deliveryFee(50, false): all three decisions arefalse, fee stays 15 and line 12 returns 15. It matches.deliveryFee(250, true): line 6 istrue, so fee becomes 0; line 9 istrue, so fee becomes 10. It returns 10, as required.deliveryFee(200, false):200 > 200isfalse, so line 7 is skipped and the function returns 15 where the requirement expects 0. This input takes the same path as 50, and only the boundary value shows the defect. Run the test to confirm the failure, then report it with the input, the expected and actual results and line 6.
1 function deliveryFee(total, express) {
2 if (total <= 0) {
3 throw new Error('Order total must be positive');
4 }
5 let fee = 15;
6 if (total > 200) {
7 fee = 0;
8 }
9 if (express) {
10 fee = fee + 10;
11 }
12 return fee;
13 }deliveryFee for four inputs: 200 takes the same path as 50 because 200 > 200 is false, so it returns 15 where the requirement expects free delivery.| Call arguments: total, express | Line 2total <= 0 | Line 6total > 200 | Line 9express | Lines run | Returns |
|---|---|---|---|---|---|
0, false | true | not reached | not reached | 2, 3 | throws Errorrequirement: rejectedPass |
50, false | false | false | false | 2, 5, 6, 9, 12 | 15requirement: 15Pass |
250, true | false | true | true | 2, 5, 6, 7, 9, 10, 12 | 10requirement: 10Pass |
200, false | false | false | false | 2, 5, 6, 9, 12 | 15requirement: 0Fail |
- Condition
true: the block under it runs - The boundary value: same path as 50, wrong result
A dash means the line was never reached: the first call stopped at the throw on line 3.
Common mistakes
- Reading a single
=as a comparison:fee = 0changes the fee; comparisons are written===,!==,==,<,<=,>or>=. - Taking the expected result from the code you just traced. The trace tells you what the code does; what it should do comes from the requirement or the product owner.
- Assuming every line runs: after a
returnor athrowthe rest of the function is skipped, and only one block of anif/else if/elsechain runs. - Tracing one happy-path input and stopping. Take each decision both ways, put a value on both sides of every comparison, and run each loop zero, one and several times.
- Ignoring brackets and short-circuits in conditions:
a || b && cis not(a || b) && c, and the second side of&&is never evaluated when the first isfalse. - Keeping the trace in your head. Write the table: loops and reassigned variables are where mental traces go wrong.
The QA perspective
On a team you read code when a change arrives. In the pull request, look for the conditions, limits and error paths it adds or changes, and turn each into a test idea or a question ("line 6 says > 200, the story says 200 or more: which is right?"). Reading code without running it is static testing; a hand trace is a quick form of it, sometimes called a dry run or desk check, and it works best with the developer beside you.
Record what you find as you would any other test: the input, the lines run, the expected result from the requirement and the actual result. A trace points to a suspected defect; where you can, run the test to observe the failure it causes. Naming the input and the line you suspect in a bug report saves the developer time, but the report still describes the observed failure, not just "the code is wrong". You do not have to propose the fix; if you do, label it as a suggestion.
Practice exercise
Trace a cart total function
Requirement: "The cart total is the sum of price × quantity for every item. Coupon SAVE10 takes 10.00 SAR off totals of 50.00 SAR or more." Trace the function below by hand for the three calls listed under it.
For each call, record the lines run, the outcome of every decision, the value of total after each step and the return value. Then compare with the expected result from the requirement and describe any defect you find.
1 function cartTotal(items, coupon) {
2 let total = 0;
3 for (let i = 1; i < items.length; i++) {
4 total = total + items[i].price * items[i].qty;
5 }
6 if (coupon === 'SAVE10' && total >= 50) {
7 total = total - 10;
8 }
9 return total;
10 }
A cartTotal([{ price: 30, qty: 2 }], 'SAVE10')
B cartTotal([{ price: 20, qty: 1 }, { price: 15, qty: 2 }], '')
C cartTotal([], 'SAVE10')- Draw the trace table: input, lines run, decisions,
totalafter each step, returned value. - Trace calls A, B and C line by line, counting the loop rounds.
- Work out the expected result of each call from the requirement alone.
- Compare, name the line you suspect and write a one-sentence defect summary.
Show the model answer
A: line 2 sets total to 0. Line 3 starts i at 1, and 1 < 1 is false, so the loop body never runs. Line 6: the coupon matches but 0 >= 50 is false, so line 7 is skipped. Returns 0. Expected: 60 - 10 = 50. The test fails.
B: total starts at 0. Round 1: i is 1, 1 < 2 is true, total becomes 0 + 15 × 2 = 30. Round 2: i is 2, 2 < 2 is false, the loop ends. Line 6: '' === 'SAVE10' is false, so && stops there. Returns 30. Expected: 20 + 30 = 50. The test fails.
C: the loop runs zero times and the coupon check is false. Returns 0, as expected. This call passes although the code is wrong: an empty cart hides the defect.
Defect: line 3 starts the loop at i = 1, so the first item is never added; it should start at i = 0. Summary: "Cart total leaves out the first item: one item at 30.00 × 2 with SAVE10 returns 0 instead of 50.00." After the fix, re-run A, B and C to confirm it (confirmation testing). Call A now reaches line 7 for the first time, so also test the coupon at its boundary with totals of 49.99 and 50.00.
Frequently asked questions
Do I need to learn programming to read code like this?
No. To trace logic you need to recognise values, comparisons, decisions, loops, return and throw, which this lesson covers. Writing your own code comes later, in the test automation lessons. Start with small functions and with pull requests your team already discusses.
What if my team does not use JavaScript?
The building blocks are the same in other languages. Java and C# use the same &&, || and !, curly braces and for loops. Python writes and, or and not, marks blocks by indentation instead of braces, and its for loop walks over the items of a list directly. A trace table works for all of them.
What is the difference between == and ===?
=== (strict equality) is true only when type and value are both the same. == (loose equality) converts types first, so '5' == 5 and 0 == '' are both true. That is why many teams require ===; when you see == in a comparison with user input, test with a number written as text and with an empty value.
Where can a tester find code to read?
In the pull request or merge request of the story you are testing, in the unit tests next to the code, in a coverage report, which shows the source line by line, and for web pages in the browser developer tools, where the Sources or Debugger panel shows the scripts the page loaded. Ask for read access to the repository; many teams give it to testers.
References
- ECMAScript® Language Specification (ECMA-262)The JavaScript language standard, including the RegExp grammar and semantics, JSON.parse/JSON.stringify and the Date object.
- JavaScript — MDNMDN's JavaScript guide and reference: the language's syntax, types, objects and built-ins as browsers implement them.
- ISTQB Certified Tester Foundation Level (CTFL) v4.0The foundation syllabus of software testing: test levels and types, test design techniques, test management and tools.
- ISTQB GlossaryThe standard definitions of testing terms — test case, regression testing, severity, priority, boundary value analysis and the rest.
Practise with Mutqan tools
Beautify JavaScript and TypeScript code with consistent indentation and spacing.
Generate boundary values (min-1, min, min+1, nominal, max-1, max, max+1) for numeric and length constraints.
Build decision tables from conditions and actions, generate all rule combinations and collapse impossible ones.
Compare two texts line by line or word by word and highlight differences.
Go deeper
Your progress on this topic
Not started
Mark the topic complete when you have studied it and done the exercise.
Sign in to track your progress
Mark topics complete, continue where you left off and unlock specialisation exams.