التفكير بالحالات الحدّية
في هذا الدرس 11 موضوعات
هذا الدرس متاح حاليًا باللغة الإنجليزية فقط، ولم تُنشر نسخته العربية بعد.
Lesson Overview
Your plan is written and you have traced it on a typical example. It works. Then someone passes an empty array, or a negative number, or a value exactly on a boundary, and the program crashes or quietly gives a wrong answer. These unusual inputs are called edge cases, and they are where most bugs live.
On the trip map, a typical example is the main road. Edge cases are the bridge that is closed, the road that ends, and the fork where the signs disagree. A careful traveller checks them before leaving.
In this lesson you will learn a checklist of edge-case types, how to write an edge-case list with an expected result for each item, and how to do it before writing code. Why this matters: testing only the happy path is a common reason that "working" code fails in real use, and a list written before coding costs far less than the same bug found by a user.
The previous lesson, "Hand-tracing with a variable table", showed how to run an input on paper. This lesson chooses which inputs to run.
Learning Objectives
By the end of this lesson you will be able to:
- Define an edge case and explain why happy-path testing is not enough.
- Use a checklist (empty, one, two, equal, extremes, negative, zero, duplicates, invalid, boundaries) to generate edge cases.
- Write an edge-case list with an expected result for each case.
- Decide, and record, what the program should do for inputs with no sensible answer.
- Turn the list into test inputs and run them.
Written Explanation
What an edge case is
An edge case is an input at the limit of what the problem allows: the smallest, the largest, the empty, the first, the last, the exactly-on-the-boundary, or the invalid. A happy path is the ordinary input that everyone thinks of first, such as five different positive numbers.
Code that works on the happy path often fails at the edges. A loop starts at index 1 and assumes element 0 exists. A variable starts at 0 and assumes some value is larger. A division assumes the divisor is not zero.
The checklist
You do not need to be clever to find edge cases. Walk through a checklist and ask each question about your inputs:
| Type | Question | Examples |
|---|---|---|
| Empty | What if there is nothing? | {}, "" |
| One | What if there is exactly one item? | {7} |
| Two | What if there are just two? | {3, 9} |
| All equal | What if every item is the same? | {5, 5, 5} |
| Duplicates | What if the answer value repeats? | {9, 9, 7} |
| Position | What if the answer is first or last? | {9, 1, 2}, {1, 2, 9} |
| Negative | What if values are below zero? | {-7, -3, -9} |
| Zero | What if a value is zero? | {0, 0} |
| Extremes | What is the largest or smallest allowed value? | 100 or 0 for a percentage |
| Boundaries | What happens exactly on a limit, and one step either side? | 49, 50, 51 for a pass mark of 50 |
| Invalid | What if the input breaks the rules? | grade 105, text with letters where digits are needed |
Not every row applies to every problem. Ask, so that you choose to skip a row rather than forget it.
Boundaries deserve three values
For "pass if the mark is at least 50", test 49, 50, and 51. A condition written > 50 instead of >= 50 is only exposed by the middle value.
The list needs expected results
An edge case is useful only if you know what the right answer is. For every case, write the input and the expected result. Writing the expectation forces you to make a decision, and decisions are what the problem statement usually left out.
Some inputs have no sensible answer, such as the largest value of an empty array. You have three honest options, and you must pick one on purpose: refuse the input (the caller must check first), return a documented special value, or report the problem to the user. Do not let the program crash by accident or invent a value that looks real. Record the choice as a constraint.
Do it before the code
Write the list after the restatement and before the code. The list shapes the code (if you know empty input is possible, you write the guard in the first draft), and a list written after the code tends to contain only the cases the code happens to handle. Each line becomes a call you can run: input -> expected. A plain main that prints results is enough for now.
Examples
Example 1: the largest value in an array (simple)
Edge-case list:
| Input | Expected | Why it matters |
|---|---|---|
{4, 9, 7} | 9 | Happy path |
{7} | 7 | One element: the loop has nothing to compare |
{5, 5, 5} | 5 | All equal |
{-7, -3, -9} | -3 | All negative |
{9, 1, 2} and {1, 2, 9} | 9 | Largest first and last |
{} | no answer | Decision needed |
A tempting first version starts the maximum at 0:
static int largestV1(int[] a) {
int max = 0;
for (int v : a) if (v > max) max = v;
return max;
}It works on {4, 9, 7}, but on {-7, -3, -9} it returns 0, a value that is not even in the array. The list caught what a happy-path test would never see. The fix starts the maximum at the first element:
static int largest(int[] a) {
int max = a[0];
for (int i = 1; i < a.length; i++) if (a[i] > max) max = a[i];
return max;
}Running both on {-7, -3, -9} printed 0 for the first version and -3 for the second. The second version, in turn, fails on the empty array:
Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: Index 0 out of bounds for length 0
at ...The program stops with an error message. The decision for the empty case: "the caller must check a.length > 0 first, and prints 'no values' otherwise". We write it in the constraints instead of discovering it at run time.
Example 2: the library late fee (practical)
From the first lesson: 50 cents per late day, a maximum of 500 cents, and the due day is not late. Edge cases with expected results for a 14-day loan:
| Days kept | Expected fee | Why |
|---|---|---|
| 13 | 0 | Before the due day |
| 14 | 0 | The boundary: on the due day |
| 15 | 50 | One day late |
| 0 | 0 | Returned at once |
| 23 | 450 | Nine late days, just below the cap |
| 24 | 500 | The cap is reached exactly |
| 100 | 500 | Far over the cap |
| -1 | invalid | Negative days |
Decision for invalid input: return -1, a documented special value. The loan length also has a rule: it must be at least 1 day, otherwise invalid.
static int lateFee(int loanDays, int daysKept) {
if (loanDays < 1 || daysKept < 0) return -1;
int lateDays = daysKept - loanDays;
if (lateDays <= 0) return 0;
int fee = lateDays * 50;
if (fee > 500) fee = 500;
return fee;
}Running the list produced 0, 0, 50, 0, 450, 500, 500, -1 in that order, and lateFee(0, 5) gave -1. Every line matches. Rows 13, 14, and 15 protect the decision "the due day is not late", and rows 23 and 24 protect the cap.
Example 3: validating a username (realistic)
Rules from the first lesson: 3 to 12 characters, letters and digits only, with spaces at the ends removed first. For checking letters and digits, this example uses Character.isLetterOrDigit, a library method this course has not covered; treat it as a preview. It tests one character.
| Input | Expected | Why |
|---|---|---|
sam12 | true | Happy path |
abc | true | Shortest allowed (3) |
ab | false | One below the minimum |
abcdefghijkl | true | Longest allowed (12) |
abcdefghijklm | false | One above the maximum (13) |
sam | true | Spaces at the ends are removed |
a b c | false | Spaces inside |
sam_1 | false | Disallowed character |
| empty text, or only spaces | false | Empty after trimming |
static boolean isValidUsername(String s) {
String t = s.trim();
if (t.length() < 3 || t.length() > 12) return false;
for (int i = 0; i < t.length(); i++) {
if (!Character.isLetterOrDigit(t.charAt(i))) return false;
}
return true;
}Running these inputs gave exactly the expected column (for example [ ab ] -> false and [ sam ] -> true). Notice how many rows come from the limits (2, 3, 12, 13) and how few from "normal" names.
Backend Insight
Real systems receive input from users and other programs, and neither follows your assumptions. Empty fields, missing data, huge payloads, negative amounts, and boundary dates arrive every day. A backend that only handles the happy path works in the demo and fails in production, where the cost is lost orders, wrong invoices, or security problems.
Edge-case thinking is therefore built into professional work: requirements discussions ask "what if the cart is empty?", reviewers ask "what happens at exactly the limit?", and test suites are built from lists like the ones here.
Common Mistakes
Mistake 1: Testing only the happy path
- Incorrect: "It worked on
{4, 9, 7}, so it is correct." - Correct: "It worked on the typical case. I also ran empty, one element, all equal, and all negative."
Mistake 2: Writing the list after the code
A list written after coding mostly confirms what the code does. Write it first.
Mistake 3: Listing cases without expected results
"Try an empty array" is not a test. Say what should happen; if you do not know, that is a question for the problem statement.
Mistake 4: Testing the boundary but not its neighbours
Test one below, on, and one above the boundary.
Exercises
Try each exercise yourself before opening the solutions.
Level A: Foundation
A1. What is an edge case, and what is the happy path?
أظهر الحل A1
A1. An edge case is an input at a limit of what the problem allows, such as empty, minimum, maximum, boundary, or invalid. The happy path is the ordinary, typical input everyone thinks of first.
A2. Name five types of edge case from the checklist.
أظهر الحل A2
A2. Any five of: empty, one, two, all equal, duplicates, position (first or last), negative, zero, extremes, boundaries, invalid.
A3. A rule says "a discount applies when the basket is at least 100". Which three basket values test the boundary?
أظهر الحل A3
A3. 99 (no discount), 100 (discount, the boundary itself), and 101 (discount).
Level B: Application
B1. List at least six edge cases, with expected results, for "return the smallest number in an array". Decide what to do for the empty array.
أظهر الحل B1
B1.
| Input | Expected |
|---|---|
{4, 9, 7} | 4 |
{7} | 7 |
{5, 5, 5} | 5 |
{-7, -3, -9} | -9 |
{2, 9, 8} and {9, 8, 2} | 2 |
{0, 3} | 0 |
{} | no answer |
Decision for the empty array: the caller must check that there is at least one value first, and it is a constraint of the method.
B2. The rule is "a password must be at least 8 characters long". List the edge cases and expected results for the length.
أظهر الحل B2
B2. Length 7 gives rejected, length 8 gives accepted (the boundary), length 9 gives accepted, length 0 (empty) gives rejected, and a very long password gives accepted unless a maximum is also specified (a question to ask).
B3. This method should return the average of an int[] as a double. List three inputs that expose problems, and say what each one would do.
static double average(int[] a) {
int sum = 0;
for (int v : a) sum += v;
return sum / a.length;
}أظهر الحل B3
B3. {}: sum / a.length is an integer division by zero, which stops the program with an ArithmeticException ("/ by zero"). {1, 2}: the division is done on integers before it becomes a double, so 3 / 2 gives 1 and the result is 1.0 instead of 1.5. {-5, 5} gives 0.0, correctly, and checks that negatives work. Running them printed 1.0, 0.0, and the exception. (With a (double) cast the empty array would give NaN, "not a number", which still needs a decision.)
Level C: Challenge
C1. For "count how many times a given letter appears in a text", write an edge-case list of at least eight rows with expected results. Then write the method and run your list.
أظهر الحل C1
C1. Example list for counting 'a' in a text: "" gives 0; "a" gives 1; "b" gives 0; "aaa" gives 3; "banana" gives 3; "Banana" depends on case (decision: uppercase A is not counted, so "Aa" gives 1); "a a" gives 2; first and last position "abca" gives 2. Method:
static int countLetter(String text, char letter) {
int count = 0;
for (int i = 0; i < text.length(); i++) {
if (text.charAt(i) == letter) count++;
}
return count;
}Running it on the list gave 0 1 0 3 3 1 2 2, matching.
C2. For "return the position of the first occurrence of a value in an array, or -1 if absent", list the edge cases and write the method.
أظهر الحل C2
C2. Cases: empty array gives -1; one element present gives 0; one element absent gives -1; value at the first position gives 0; value at the last position gives length - 1; repeated value such as {4, 7, 7} and 7 gives 1 (the first). Method:
static int indexOfFirst(int[] a, int value) {
for (int i = 0; i < a.length; i++) {
if (a[i] == value) return i;
}
return -1;
}Returning inside the loop at the first match guarantees the first occurrence. Running the empty, repeated, and absent cases gave -1 1 -1.
Mini Challenge
Choose "check whether a text is a palindrome (reads the same forwards and backwards)".
- Restate the problem and decide: are uppercase and lowercase the same, and are spaces ignored?
- Write an edge-case list of at least eight rows with expected results, using the checklist.
- Write the method and run every row.
Check your work: the list came before the code, each row has an expected result, and your decisions are written down.
Summary & Next Lesson
Key takeaways
- Edge cases are inputs at the limits where code often breaks. Happy-path testing misses most bugs.
- Use the checklist: empty, one, two, equal, duplicates, position, negative, zero, extremes, boundaries, invalid.
- Test a boundary on both sides and on the value itself.
- Write each case with an expected result. When there is no sensible answer, decide what happens and write it down.
- Write the list before the code, then run it on the finished code.
Next lesson
You now have all four parts of the routine: understand, plan, trace, and list edge cases. The next lesson, "Demo: plan before code on an unseen problem", puts them together on one problem from start to finish.
Self-check
- Name three edge cases that apply to almost any problem about a list.
- Why test 49, 50, and 51 for a pass mark of 50?
- Why must each edge case have an expected result?
- When should you write the edge-case list: before or after the code?
- What are three honest options for an input with no sensible answer?
أظهر الإجابات
- Any three of: empty, one element, all equal, duplicates, negatives, first or last position.
- To test the boundary itself (50) and both sides, so that a
>written instead of>=is exposed. - Without one you cannot tell whether the program passed, and writing it forces a decision the statement left out.
- Before the code, so the list shapes the code and is an honest test of it.
- Refuse the input (the caller must check first), return a documented special value, or report the problem to the user.
References
- Oracle, Java SE 21 API:
Character. Supports thatCharacterprovides tests such asisLetteron achar;isLetterOrDigitis the combined test used in the username example (its exact wording on the page was not read in full). - Oracle, Java SE 21 API:
String. Supportstrim,length, andcharAt. - The Java examples (
largestV1,largest,lateFee,isValidUsername, and the empty-array crash andArithmeticExceptionmessages) were compiled and run with JDK 21 and the output shown is real. The exercise solutions B3, C1, and C2 were also compiled and run.
التطبيق
طبّق هذا الدرس بمختبر الوحدة وتمارينها واختبارها.
تعليم الدرس كمكتمل اختياري ويمكنك التراجع عنه في أي وقت. يبقى تقدمك محفوظًا في هذا المتصفح.