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

تشريح حالة اختبار

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

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

Lesson Overview

Lesson 1 showed that a test is a program that checks a result for you. This lesson is about what a good one looks like. A test case is small and has a fixed shape, and once you can see that shape you can write tests quickly and read other people's tests at a glance.

You will learn the three parts of every test case (input, action, expected result), the Arrange, Act, Assert pattern, how to name a test so a failure explains itself, why tests must be independent, and why a test with no assertion proves nothing. We keep using plain Java and a tiny helper, so the ideas are clear before JUnit adds its own vocabulary in the next lesson.

The inspector with a checklist returns: each line of a checklist names one thing to check and one right answer. A test case is one line of that checklist, written as code.

Learning Objectives

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

  • Name the three parts of a test case: input, action, and expected result.
  • Write a test in the Arrange, Act, Assert pattern and label each part.
  • Choose a test name that states the behavior being checked.
  • Explain why a test should check one behavior.
  • Show, with an example, how tests that share state can depend on each other, and fix it.
  • Recognize a test with no assertion and explain why it can never fail.

Written Explanation

What a test case contains

Every test case answers one question: when I do this, with this, do I get that? It has three parts:

PartQuestionExample for Account
InputWhat state and arguments do I start with?A new account with 100, and the amount 50
ActionWhat single thing do I call?deposit(50)
Expected resultWhat must be true afterwards?getBalance() is 150

The expected result is the part beginners skip. Without it you have a program that runs, not a test.

Arrange, Act, Assert

The standard layout of a test body has three steps, in this order:

  1. Arrange: build the objects and values the test needs.
  2. Act: call the code you are testing (usually one call).
  3. Assert: check that the result is what you expected.
java
static void depositIncreasesBalance() {
    // Arrange
    Account account = new Account("Sam", 100);
    // Act
    account.deposit(50);
    // Assert
    Check.equal(150, account.getBalance());
}

Here Check.equal(expected, actual) is the small helper from lesson 1 (it throws AssertionError when the two numbers differ). Keep the three parts visually separate, with blank lines or comments. When a test fails, you can then tell at once whether the setup, the call, or the check is the part to look at. Arrange, Act, Assert is usually shortened to AAA.

A useful rule: if you find yourself writing a second Act after the Assert, you are probably testing two behaviors and should split the test.

One behavior per test

A good test checks one behavior. "Deposit increases the balance" is one behavior. "Deposit increases the balance, and withdraw decreases it, and a negative deposit is rejected" is three, and if the test fails, the failure does not tell you which one broke. One behavior does not mean exactly one assert line: checking that a method returns the right value and left the object in the right state can belong to the same behavior. It means one reason to fail.

Naming a test

When a test fails, the name is the first thing you read. Name it after the behavior, not after the method or a number:

WeakBetter
test1depositIncreasesBalance
testWithdrawwithdrawMoreThanBalanceIsRejected
depositdepositOfZeroIsRejected

A good name reads as a sentence about the code: "deposit increases balance". If you cannot name the test without using "and", it probably checks too much.

Independent tests

Tests must be independent: each one must pass or fail on its own, in any order, and running one alone must give the same result as running it with the others. The usual way to break this is shared state: two tests use the same object, so the first test's changes leak into the second.

The simplest cure is the one in the AAA example: every test arranges its own objects. JUnit helps by creating a fresh instance of the test class for every test method, but that protects only instance fields; anything static is still shared, and so is anything stored outside the program, such as a file or a database. Treat each test as if it were the only one on Earth.

A related rule: a test should not need another test to have run first, and it should not leave anything behind that another test depends on. If your tests only pass in one order, they are chained together, and a failure in the first will show up as a confusing failure in the second.

A test without an assertion

Consider this:

java
static void t() {
    new Account("Sam", 100).deposit(5);
}

It has input and action but no expected result. It can fail only if the code throws an exception. If deposit silently does the wrong thing, this "test" still passes. It gives you a green result and no information. Always ask of a test: what would make this fail? If the answer is "nothing", it is not a test.

Examples

Example 1: labeling a test (simple)

Take the Account rule "deposit adds the amount". As a test case table, it is: input = account with 100 and amount 50, action = deposit(50), expected = balance 150. As code, the same case is the depositIncreasesBalance method in the AAA section above. We compiled and ran it against the correct Account; it passed.

Practice reading a test by labeling: the first line builds the account (Arrange), the second calls deposit (Act), the third compares (Assert). If every test in a file follows this shape, you can skim a test file in seconds.

Example 2: tests that depend on each other (practical)

Two tests that share one static Account:

java
static Account shared = new Account("Sam", 100);

static void depositAddsFifty() {
    shared.deposit(50);
    Check.equal(150, shared.getBalance());
}

static void withdrawSubtractsThirty() throws InsufficientFundsException {
    shared.withdraw(30);
    Check.equal(70, shared.getBalance());
}

Each test is correct by itself. But run in a different order, they give different results. Real output from our small runner (JDK 21), once in each order:

text
PASS depositAddsFifty
FAIL withdrawSubtractsThirty: expected 70.0 but was 120.0

PASS withdrawSubtractsThirty
FAIL depositAddsFifty: expected 150.0 but was 120.0

Whichever test runs second sees the first one's changes. In both orders the code under test is fine; the tests are the problem. JUnit does not promise a convenient order for your test methods, so a suite like this would be fragile in practice. The fix is to create the account inside each test:

java
static void depositAddsFifty() {
    Account account = new Account("Sam", 100);
    account.deposit(50);
    Check.equal(150, account.getBalance());
}

With the account arranged inside each test (and the same for withdrawSubtractsThirty), both orders printed PASS for both tests.

Example 3: a test that cannot fail (realistic)

Suppose a teammate breaks deposit (it assigns instead of adding) and you run these two tests against the broken class:

java
static void t()      { new Account("Sam", 100).deposit(5); }

static void tFixed() {
    Account account = new Account("Sam", 100);
    account.deposit(5);
    Check.equal(105, account.getBalance());
}

Real output:

text
t passed
Exception in thread "main" java.lang.AssertionError: expected 105.0 but was 5.0
	at Check.equal(Check.java:4)
	at NoAssert.tFixed(NoAssert.java:8)
	at NoAssert.main(NoAssert.java:13)

The test with no assertion passed against broken code; the one with an assertion caught it. In a real team the first kind is dangerous: it counts toward "we have 200 tests" without protecting anything.

Backend Insight

Backend test suites grow to hundreds or thousands of tests, run by machines, often in parallel or in an order you do not control. Independence is what makes that safe. Shared mutable state (a static list, a file on disk, a row in a database) is the most common reason for a suite that passes on one computer and fails on another, or fails only sometimes.

The Arrange step often becomes the longest part of a backend test (building an order, a customer, an account with some history). When Arrange gets long, that is useful feedback: the class may have too many requirements. Later modules show ways to share setup code safely.

Common Mistakes

Mistake 1: A test with no assertion

  • Incorrect: calling the method and ending the test.
  • Correct: end every test with a check of a result or of an expected exception.

Why it happens: the author checked by eye while writing it. How to avoid it: ask "what would make this fail?" before moving on.

Mistake 2: Testing only the happy path

A suite that checks only valid, typical input misses the bugs that live in bad input and edge cases. Lessons 4 and 5 cover them.

Mistake 3: Tests that depend on each other

  • Incorrect: testB works only if testA ran first and left the account at 150.
  • Correct: each test builds the state it needs.

Mistake 4: Vague names and giant tests

  • Incorrect: test1, or one test with twelve asserts about five behaviors.
  • Correct: withdrawMoreThanBalanceIsRejected, one behavior per test.

Exercises

Level A: Foundation

A1. Name the three parts of a test case and the three steps of AAA.

أظهر الحل A1

A1. Input, action, expected result. Arrange, Act, Assert.

A2. Rename each test so the name states the behavior: test2 (checks that a new Account with a negative opening balance is rejected), withdraw (checks that withdrawing 40 from 100 leaves 60), account (checks that getBalance() of a new account equals the opening balance).

أظهر الحل A2

A2. For example: negativeOpeningBalanceIsRejected, withdrawSubtractsFromBalance, newAccountStartsWithOpeningBalance. Any name that states the behavior is fine.

A3. Why is a test with no assertion not a real test?

أظهر الحل A3

A3. It has no expected result, so it can only fail by an accident (an exception). If the code returns a wrong value, it still passes, so it gives no information.

Level B: Application

B1. Label the parts of this test (Arrange, Act, Assert) and say what behavior it checks:

java
static void t() throws InsufficientFundsException {
    Account a = new Account("Ana", 100);
    a.withdraw(40);
    Check.equal(60, a.getBalance());
}
أظهر الحل B1

B1. Account a = new Account("Ana", 100); is Arrange. a.withdraw(40); is Act. Check.equal(60, a.getBalance()); is Assert. It checks that a withdrawal reduces the balance by the amount (for example, withdrawSubtractsFromBalance).

B2. Write a test in AAA form (with Check.equal) for: a new account with 100, deposit(25), expected balance 125.

أظهر الحل B2

B2.

java
static void depositIncreasesBalance() {
    Account account = new Account("Sam", 100);
    account.deposit(25);
    Check.equal(125, account.getBalance());
}

We compiled and ran this alongside the other examples; it passed.

B3. Explain why this pair of tests is fragile and how to fix it: both read and change a static Account shared, and the second expects the first to have run.

أظهر الحل B3

B3. The second test depends on state left by the first, so it passes only in one order (and not when run alone). Create a fresh Account inside each test.

Level C: Challenge

C1. Write a test case table (input, action, expected result) for three different behaviors of withdraw on an account with 100. Do not include exceptions yet.

أظهر الحل C1

C1. Example:

InputActionExpected result
Account with 100withdraw(40)balance 60
Account with 100withdraw(100)balance 0
Account with 100withdraw(1)balance 99

C2. A single test creates an account, deposits 50, checks 150, withdraws 30, checks 120, then deposits a negative amount inside try/catch and checks nothing. List the problems and split it into better tests.

أظهر الحل C2

C2. Problems: it checks several behaviors, so a failure does not name the broken one; the later steps depend on the earlier ones; the last part has no assertion, so it passes whether or not the negative deposit is rejected. Split into depositIncreasesBalance, withdrawDecreasesBalance, and (after lesson 4) a test that expects the negative deposit to be rejected, each with its own account.

Mini Challenge

Pick the method from your Mini Challenge in lesson 1, or a new small method. Write three test cases as a table (input, action, expected result), then code each as a separate AAA method with a behavior name. Run them in two different orders by changing the order of the calls in main.

Check your work: every test has all three parts and labeled sections, no test shares an object with another, the results are identical in both orders, and you can say which assertion would fail if the method were broken.

Summary & Next Lesson

Key takeaways

  • A test case has an input, an action, and an expected result.
  • Arrange, Act, Assert keeps tests readable and tells you where to look when one fails.
  • One behavior per test, named after the behavior.
  • Tests must be independent: each builds its own state and passes in any order.
  • A test with no assertion proves nothing.

Next lesson

The next lesson, "First JUnit 5 tests", replaces our hand-made helper with JUnit 5: @Test, the standard assertions, and running tests with the IDE.

Self-check

  1. What are the three parts of a test case?
  2. What does each letter of AAA stand for?
  3. What is wrong with a test named test1?
  4. Why do tests that share a static object become fragile?
  5. A test calls deposit(5) and never checks anything. Can it fail?
أظهر الإجابات
  1. Input, action, expected result.
  2. Arrange, Act, Assert.
  3. The name does not say which behavior is checked, so a failure explains nothing.
  4. One test's changes leak into the next, so the result depends on the order the tests run in.
  5. Only if deposit throws an exception. It will not detect a wrong balance.

References

  • JUnit documentation, Test Instance Lifecycle. Supports the statement that JUnit Jupiter by default creates a new instance of the test class before each test method, to keep tests isolated.
  • JUnit documentation, Test Execution Order. Supports the statements that the default method order is deterministic but intentionally not obvious, and that tests should generally not depend on order.
  • Arrange, Act, Assert is a widely used convention; I did not cite a single authoritative source for it, so treat the name as common practice rather than a standard. All code and output above were compiled and run on JDK 21 with the Account from the exceptions module (BEG-029).

التطبيق

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

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

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

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