انتقل إلى المحتوى
أمين أبوزيد
منشورBEG-045-L01

لماذا نختبر آلياً

  • الدرس 1 من 7
  • 11 دقيقة قراءة
  • مبتدئ
في هذا الدرس 11 موضوعات

هذا الدرس متاح حاليًا باللغة الإنجليزية فقط، ولم تُنشر نسخته العربية بعد.

Lesson Overview

So far you have checked your programs the way most beginners do: run main, look at the printed output, and decide with your eyes whether it is right. That works for a ten-line program. It stops working when you have a bank with deposits, withdrawals, and rules, and you change one method on a Tuesday afternoon.

In this lesson you will see why professional teams write automated tests: small programs that run your code and check the result for you. You will compare a manual check with an automated one on the same Account class from the exceptions module, watch the automated one catch a bug that the manual one lets slip by, and learn the word regression.

We use plain Java only in this lesson. The testing library, JUnit 5, arrives in lesson 3. First you need to understand what problem it solves.

The analogy for the whole module is a safety inspector with a checklist: the inspector checks the same things, in the same way, every single time, and writes down pass or fail. An automated test is that inspector, written in code.

Learning Objectives

By the end of this lesson you will be able to:

  • Explain what an automated test is and how it differs from checking by hand.
  • Give at least three reasons automated tests are worth writing.
  • Define a regression and explain how tests catch one.
  • Write a tiny self-checking program that fails loudly when a result is wrong.
  • Explain how tests act as documentation and why they make changing code less frightening.
  • Name the costs of testing honestly, so you do not expect tests to prove everything.

Written Explanation

Checking by hand

Take the Account class from the exceptions module (it has deposit, withdraw, and getBalance). A manual check looks like this:

java
public class ManualCheck {
    public static void main(String[] args) {
        Account a = new Account("Sam", 100);
        a.deposit(50);
        System.out.println("Balance: " + a.getBalance());
    }
}

Output:

text
Balance: 150.0

You look at 150.0, think "that is right", and move on. Notice what this check depends on: you. You must remember to run it, remember what the right answer is, read the output carefully, and do all of that again after every change. Checking by hand has three weaknesses:

  • It is slow: for fifty behaviors you would read fifty lines of output every time.
  • It is easy to forget: nobody re-checks the deposit after editing withdraw.
  • It is inconsistent: today you check three things, tomorrow five, and neither of you wrote down which.

An automated test

An automated test is a small program that runs a piece of your code, compares the result with the answer you expect, and reports pass or fail by itself. Here is the smallest version, without any library:

java
public class HandCheck {
    public static void main(String[] args) {
        Account a = new Account("Sam", 100);
        a.deposit(50);
        if (a.getBalance() != 150) {
            throw new AssertionError("expected 150 but was " + a.getBalance());
        }
        System.out.println("deposit check passed");
    }
}

AssertionError is a standard JDK error type meant for "a check that should hold did not". The program now contains the expected answer (150) and the decision (compare). Nobody has to read anything: when the balance is wrong, the program stops with an error and a message. When it is right, it prints one calm line.

Three things changed. The expected answer is written down once, in code. The comparison is done by the computer, so it is identical every time. And you can run it as often as you like, in a fraction of a second.

Regressions

A regression is a feature that worked and broke after a change. Regressions are the most common bug in a growing codebase, because the change that breaks the feature is usually in a different place from the feature.

Imagine a teammate tidies up deposit and accidentally writes balance = amount; instead of balance += amount;. Here is what each kind of check says:

text
Manual check:      Balance: 50.0
Automated check:   Exception in thread "main" java.lang.AssertionError: expected 150 but was 50.0
                       at HandCheck.main(HandCheck.java:6)

The manual check still prints a number, and 50.0 looks as plausible as 150.0 if you are not paying attention. The automated check cannot be fooled by plausibility, because it knows the answer.

Why teams write tests

ReasonWhat it means
SpeedA suite of hundreds of tests runs in seconds, many times a day
RepeatabilityThe same checks run the same way every time, with no one forgetting
Regression safetyOld behavior is re-checked after every change
DocumentationA test shows, in code that is checked, how a method is supposed to behave
ConfidenceYou can change or clean up code and find out quickly whether you broke something

The last point is the one experienced engineers value most. Martin Fowler describes how teams without good tests can become afraid to touch old code, even to fix bugs, because any change might break something else. With tests, the code can be kept clean.

Tests are living documentation because, unlike a comment, a test that no longer matches the code fails. A comment can quietly become false. A test cannot.

What tests cannot do

Be honest about limits. A test only checks what you thought to check: passing tests do not prove the program has no bugs. Tests also take time to write and to keep up to date. And a badly written test (one that checks nothing, or checks the wrong thing) gives false comfort, which is worse than no test. The rest of this module is about writing tests that are worth their cost.

Where this fits in a workflow

A common team habit: when a bug is reported, first write a test that reproduces it (it fails), then fix the code (the test passes). The bug can never return unnoticed, because the test stays. This is the idea behind the planned mistake you will meet in the last lesson of the module.

Examples

Example 1: a manual check and its self-checking twin (simple)

Both programs from the Written Explanation, side by side, run against the correct Account:

text
Balance: 150.0
deposit check passed

The manual program tells you a fact and leaves the judging to you. The self-checking program makes the judgment. Only the second can run unattended.

Example 2: catching a regression (practical)

We copied Account, changed balance += amount; to balance = amount; in the copy, and ran the same two programs. This is real output from JDK 21:

text
Balance: 50.0
Exception in thread "main" java.lang.AssertionError: expected 150 but was 50.0
	at HandCheck.main(HandCheck.java:6)

Read the second result as a sentence: the code returned 50.0 when 150 was expected, and the line that complained is line 6 of HandCheck. The next lessons teach you to read these messages quickly (lesson 6 is devoted to it).

Example 3: many checks, one answer (realistic)

A real class has more than one behavior, so a self-checking program grows several checks:

java
public class BankChecks {
    static void check(String name, double expected, double actual) {
        if (expected != actual) {
            throw new AssertionError(name + ": expected " + expected + " but was " + actual);
        }
        System.out.println("pass: " + name);
    }

    public static void main(String[] args) throws InsufficientFundsException {
        Account a = new Account("Sam", 100);
        a.deposit(50);
        check("deposit adds", 150, a.getBalance());

        a.withdraw(30);
        check("withdraw subtracts", 120, a.getBalance());
    }
}

We compiled and ran it with the correct Account:

text
pass: deposit adds
pass: withdraw subtracts

Now you have written your own tiny testing framework: a helper that compares and a name that says what was checked. Libraries such as JUnit do exactly this job, with extras (running each check separately, reporting all failures, drawing a green or red bar in your IDE). That is the subject of lesson 3.

Backend Insight

Backend code is changed constantly: new rules, new fields, new endpoints, and fixes. It also runs without a human watching, so a silent wrong answer (a wrong total, a wrong balance) can sit unnoticed for days. Teams therefore commonly run their automated tests on every change before the code goes further; this practice is usually called continuous integration, and you will meet it later in your learning. Tests are the safety inspector's checklist that makes that possible.

There is also a design effect. Code that is easy to test (small methods, clear inputs and outputs, few hidden dependencies) tends to be easy to understand and change. If you struggle to write a test for a method, that is often a hint that the method does too much.

Common Mistakes

Mistake 1: "I ran it and it looked fine"

  • Incorrect: relying on a printed value that you eyeball.
  • Correct: put the expected value in code and let the program compare.

Why it happens: printing is the first debugging tool beginners learn. How to avoid it: whenever you print a result to check it, ask what the expected value is, and move that comparison into a check.

Mistake 2: Believing green means bug-free

A passing test proves only that those cases work. Choosing the right cases is the skill of lessons 5 and 7.

Mistake 3: Testing only after everything is written

Late tests find bugs when they are expensive to fix. Write checks as you go, and, as the last lesson shows, sometimes before the code.

Mistake 4: Thinking tests are extra work you do for someone else

Tests save the author time as well: every manual re-run you replace is time you get back, starting with the second change you make.

Exercises

Try each exercise yourself before opening the solutions.

Level A: Foundation

A1. In your own words, define "automated test" in one sentence.

أظهر الحل A1

A1. An automated test is a small program that runs some code, compares the result with an expected value, and reports pass or fail without a person reading the output.

A2. Name three weaknesses of checking by hand.

أظهر الحل A2

A2. It is slow, it is easy to forget, and it is inconsistent from person to person and day to day. (Also acceptable: it needs the person to remember the right answer each time.)

A3. A feature worked on Monday. On Wednesday, after an unrelated change, it fails. What is this called?

أظهر الحل A3

A3. A regression.

Level B: Application

B1. Write a self-checking main (no library) for Account that creates an account with 100, deposits 25, and fails with an AssertionError if the balance is not 125.

أظهر الحل B1

B1.

java
public class OneCheck {
    public static void main(String[] args) {
        Account a = new Account("Sam", 100);
        a.deposit(25);
        if (a.getBalance() != 125) {
            throw new AssertionError("expected 125 but was " + a.getBalance());
        }
        System.out.println("pass");
    }
}

We compiled and ran it: it printed pass.

B2. The program System.out.println(a.getBalance()); checks the opening balance of a new account. List what a person must do for this check to be useful, and say which of those steps a self-checking version removes.

أظهر الحل B2

B2. The person must run it, know the correct value, read the output, and compare, and must repeat this after every change. The self-checking version removes the knowing, reading, and comparing during each run: the expected value is stored in the code and the computer compares.

B3. Someone says: "My comments explain how withdraw behaves, so tests add nothing." Give one reply based on this lesson.

أظهر الحل B3

B3. A comment can become false without anyone noticing when the code changes. A test that no longer matches the code fails, so it stays true. Tests are also run, not just read.

Level C: Challenge

C1. Extend BankChecks by writing one more check: after withdraw(120) on the 120 balance, the balance should be 0. Predict the result, then explain why check compares double values with != safely here, and what could go wrong with other values.

أظهر الحل C1

C1. The check passes, because 120 - 120 is exactly 0. Comparing double values with != works here because both sides are exactly representable. Decimal fractions such as 0.1 and 0.2 are stored approximately, so a computed result may differ from the expected value in the last digits. Lesson 3 shows the usual remedy: compare with a small tolerance (a delta).

C2. A teammate changes withdraw so it ignores the overdraft rule (it no longer throws InsufficientFundsException). Would BankChecks as written in Example 3 catch it? What kind of check would?

أظهر الحل C2

C2. No: Example 3 only tests a deposit and a withdraw that is allowed. A check is needed that tries to withdraw more than the balance and expects InsufficientFundsException. Testing bad input is the subject of lesson 4.

Mini Challenge

Choose a small method you have already written in this course (for example, one that returns the larger of two numbers, or one that counts words). Write a plain-Java self-checking main with at least three checks, each with a name. Then break the method on purpose (change one operator) and run your checks again.

Check your work: at least one check fails after the change; the failure message names the check and shows both the expected and the actual value; and you can say in one sentence why this found the bug faster than reading printed output.

Summary & Next Lesson

Key takeaways

  • An automated test is a small program that runs your code and checks the result against an expected value.
  • Tests are fast, repeatable, and protect against regressions: features that worked and broke after a change.
  • Tests document behavior in a way that cannot quietly go out of date, and they give you confidence to change code.
  • Passing tests do not prove the absence of bugs; they prove that the cases you chose work.

Next lesson

The next lesson, "Anatomy of a test case", names the parts of a good test: input, action, and expected result; the Arrange, Act, Assert pattern; good names; and why tests must be independent of each other.

Self-check

  1. What is a regression?
  2. Name two things a test has that a manual check does not.
  3. Why might a manual check of Balance: 50.0 fail to reveal a bug?
  4. Does a green result prove the code has no bugs?
  5. Why does a failing test beat a wrong comment?
أظهر الإجابات
  1. A feature that worked and broke after a change.
  2. The expected answer written down in code, and an automatic comparison that reports pass or fail. (Also: it can be run unattended and repeated identically.)
  3. Because the value looks plausible; the person has to remember that 150 was expected. An automated test holds the expected value.
  4. No. It shows only that the cases that were checked work.
  5. The test fails when the behavior changes, so it cannot silently become false; a comment can.

References

  • Martin Fowler, Self Testing Code. Supports the points that automated tests give confidence to change code, that teams without them can fear touching old code, and that reproducing a reported bug in a test keeps it from returning.
  • The Account and InsufficientFundsException classes are the ones built in the exceptions module (BEG-029). All code and output in this lesson was compiled and run on JDK 21 on the author's machine; the broken Account is a deliberate copy used only for the demonstration.
  • I did not fetch the Javadoc for AssertionError, so its description here ("a check that should hold did not") is a general description, not a quotation.

التطبيق

طبّق هذا الدرس بمختبر الوحدة وتمارينها واختبارها.

المختبر والتمارين الاختبار

تعليم الدرس كمكتمل اختياري ويمكنك التراجع عنه في أي وقت. يبقى تقدمك محفوظًا في هذا المتصفح.

اكتب للبحث في المستويات والمراحل والوحدات والدروس.